From 29b09e3cc3d38c4025f21036c1c7e935aa375e9a Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 2 Sep 2026 06:01:40 +0000 Subject: [PATCH 1/2] =?UTF-8?q?Agrega=20S4=20=E2=80=94=20Programaci=C3=B3n?= =?UTF-8?q?=20Orientada=20a=20Objetos=20II=20al=20curso=20de=20C#?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Añade la sección curricular s4-programacion-orientada-objetos-2 (8 unidades, 40 lecciones, 32 prácticas) al Course csharp-poo-1 existente, usando la capa de authoring declarativa colocalizada por unidad (prisma/content/courses/csharp-poo-1/units/09-colecciones..16-integrador). S3 (POO I) se reutiliza sin cambios vía adaptLegacyUnits. Las 8 unidades nuevas quedan published:false a la espera del release gate. Metadata general del Course actualizada para cubrir ambas asignaturas (title/description/subjectName); slug/language/executionProfile no cambian. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01SopFLn5n6iqCyq7zGQN4JS --- prisma/content/courses/csharp-poo-1/index.ts | 80 ++++--- .../units/09-colecciones/index.ts | 20 ++ .../lessons/01-de-arreglos-a-colecciones.ts | 139 +++++++++++ .../lessons/02-list-de-objetos.ts | 171 ++++++++++++++ .../lessons/03-stack-y-queue.ts | 132 +++++++++++ .../lessons/04-agregar-buscar-eliminar.ts | 184 +++++++++++++++ .../lessons/05-inventario-en-memoria.ts | 215 ++++++++++++++++++ .../units/09-colecciones/practice.ts | 202 ++++++++++++++++ .../units/10-diccionarios/index.ts | 20 ++ .../lessons/01-clave-y-valor.ts | 129 +++++++++++ .../lessons/02-dictionary-basico.ts | 138 +++++++++++ .../lessons/03-buscar-seguro.ts | 121 ++++++++++ .../lessons/04-actualizar-y-eliminar.ts | 134 +++++++++++ .../lessons/05-elegir-coleccion.ts | 123 ++++++++++ .../units/10-diccionarios/practice.ts | 195 ++++++++++++++++ .../units/11-ordenamiento/index.ts | 20 ++ .../lessons/01-criterio-de-orden.ts | 144 ++++++++++++ .../lessons/02-burbuja-con-objetos.ts | 128 +++++++++++ .../lessons/03-insercion-con-objetos.ts | 128 +++++++++++ .../lessons/04-icomparer-e-icomparer.ts | 137 +++++++++++ .../lessons/05-buscar-objetos.ts | 121 ++++++++++ .../units/11-ordenamiento/practice.ts | 149 ++++++++++++ .../csharp-poo-1/units/12-xml/index.ts | 20 ++ .../units/12-xml/lessons/01-que-es-xml.ts | 125 ++++++++++ .../lessons/02-bien-formado-y-valido.ts | 130 +++++++++++ .../units/12-xml/lessons/03-objetos-a-xml.ts | 123 ++++++++++ .../units/12-xml/lessons/04-xml-a-objetos.ts | 130 +++++++++++ .../12-xml/lessons/05-persistencia-xml.ts | 121 ++++++++++ .../csharp-poo-1/units/12-xml/practice.ts | 130 +++++++++++ .../csharp-poo-1/units/13-genericos/index.ts | 20 ++ .../lessons/01-por-que-genericos.ts | 132 +++++++++++ .../13-genericos/lessons/02-clase-generica.ts | 125 ++++++++++ .../lessons/03-metodo-generico.ts | 109 +++++++++ .../lessons/04-restricciones-genericas.ts | 143 ++++++++++++ .../lessons/05-repositorio-generico.ts | 127 +++++++++++ .../units/13-genericos/practice.ts | 124 ++++++++++ .../units/14-concurrencia/index.ts | 20 ++ .../lessons/01-proceso-hilo-concurrencia.ts | 108 +++++++++ .../lessons/02-thread-y-join.ts | 132 +++++++++++ .../lessons/03-estado-compartido.ts | 75 ++++++ .../lessons/04-lock-y-exclusion-mutua.ts | 111 +++++++++ .../lessons/05-errores-en-hilos.ts | 119 ++++++++++ .../units/14-concurrencia/practice.ts | 84 +++++++ .../csharp-poo-1/units/15-redes/index.ts | 21 ++ .../lessons/01-protocolo-mensaje-socket.ts | 131 +++++++++++ .../units/15-redes/lessons/02-tcp-y-udp.ts | 74 ++++++ .../15-redes/lessons/03-ip-puerto-endpoint.ts | 109 +++++++++ .../lessons/04-cliente-servidor-tcp.ts | 137 +++++++++++ .../15-redes/lessons/05-udp-y-mensajes.ts | 116 ++++++++++ .../lessons/06-seguridad-del-mensaje.ts | 123 ++++++++++ .../csharp-poo-1/units/15-redes/practice.ts | 102 +++++++++ .../csharp-poo-1/units/16-integrador/index.ts | 19 ++ .../lessons/01-requisitos-y-uml.ts | 188 +++++++++++++++ .../lessons/02-arquitectura-del-proyecto.ts | 137 +++++++++++ .../lessons/03-integracion-concurrente.ts | 111 +++++++++ .../lessons/04-entrega-y-evidencias.ts | 116 ++++++++++ .../units/16-integrador/practice.ts | 141 ++++++++++++ 57 files changed, 6529 insertions(+), 34 deletions(-) create mode 100644 prisma/content/courses/csharp-poo-1/units/09-colecciones/index.ts create mode 100644 prisma/content/courses/csharp-poo-1/units/09-colecciones/lessons/01-de-arreglos-a-colecciones.ts create mode 100644 prisma/content/courses/csharp-poo-1/units/09-colecciones/lessons/02-list-de-objetos.ts create mode 100644 prisma/content/courses/csharp-poo-1/units/09-colecciones/lessons/03-stack-y-queue.ts create mode 100644 prisma/content/courses/csharp-poo-1/units/09-colecciones/lessons/04-agregar-buscar-eliminar.ts create mode 100644 prisma/content/courses/csharp-poo-1/units/09-colecciones/lessons/05-inventario-en-memoria.ts create mode 100644 prisma/content/courses/csharp-poo-1/units/09-colecciones/practice.ts create mode 100644 prisma/content/courses/csharp-poo-1/units/10-diccionarios/index.ts create mode 100644 prisma/content/courses/csharp-poo-1/units/10-diccionarios/lessons/01-clave-y-valor.ts create mode 100644 prisma/content/courses/csharp-poo-1/units/10-diccionarios/lessons/02-dictionary-basico.ts create mode 100644 prisma/content/courses/csharp-poo-1/units/10-diccionarios/lessons/03-buscar-seguro.ts create mode 100644 prisma/content/courses/csharp-poo-1/units/10-diccionarios/lessons/04-actualizar-y-eliminar.ts create mode 100644 prisma/content/courses/csharp-poo-1/units/10-diccionarios/lessons/05-elegir-coleccion.ts create mode 100644 prisma/content/courses/csharp-poo-1/units/10-diccionarios/practice.ts create mode 100644 prisma/content/courses/csharp-poo-1/units/11-ordenamiento/index.ts create mode 100644 prisma/content/courses/csharp-poo-1/units/11-ordenamiento/lessons/01-criterio-de-orden.ts create mode 100644 prisma/content/courses/csharp-poo-1/units/11-ordenamiento/lessons/02-burbuja-con-objetos.ts create mode 100644 prisma/content/courses/csharp-poo-1/units/11-ordenamiento/lessons/03-insercion-con-objetos.ts create mode 100644 prisma/content/courses/csharp-poo-1/units/11-ordenamiento/lessons/04-icomparer-e-icomparer.ts create mode 100644 prisma/content/courses/csharp-poo-1/units/11-ordenamiento/lessons/05-buscar-objetos.ts create mode 100644 prisma/content/courses/csharp-poo-1/units/11-ordenamiento/practice.ts create mode 100644 prisma/content/courses/csharp-poo-1/units/12-xml/index.ts create mode 100644 prisma/content/courses/csharp-poo-1/units/12-xml/lessons/01-que-es-xml.ts create mode 100644 prisma/content/courses/csharp-poo-1/units/12-xml/lessons/02-bien-formado-y-valido.ts create mode 100644 prisma/content/courses/csharp-poo-1/units/12-xml/lessons/03-objetos-a-xml.ts create mode 100644 prisma/content/courses/csharp-poo-1/units/12-xml/lessons/04-xml-a-objetos.ts create mode 100644 prisma/content/courses/csharp-poo-1/units/12-xml/lessons/05-persistencia-xml.ts create mode 100644 prisma/content/courses/csharp-poo-1/units/12-xml/practice.ts create mode 100644 prisma/content/courses/csharp-poo-1/units/13-genericos/index.ts create mode 100644 prisma/content/courses/csharp-poo-1/units/13-genericos/lessons/01-por-que-genericos.ts create mode 100644 prisma/content/courses/csharp-poo-1/units/13-genericos/lessons/02-clase-generica.ts create mode 100644 prisma/content/courses/csharp-poo-1/units/13-genericos/lessons/03-metodo-generico.ts create mode 100644 prisma/content/courses/csharp-poo-1/units/13-genericos/lessons/04-restricciones-genericas.ts create mode 100644 prisma/content/courses/csharp-poo-1/units/13-genericos/lessons/05-repositorio-generico.ts create mode 100644 prisma/content/courses/csharp-poo-1/units/13-genericos/practice.ts create mode 100644 prisma/content/courses/csharp-poo-1/units/14-concurrencia/index.ts create mode 100644 prisma/content/courses/csharp-poo-1/units/14-concurrencia/lessons/01-proceso-hilo-concurrencia.ts create mode 100644 prisma/content/courses/csharp-poo-1/units/14-concurrencia/lessons/02-thread-y-join.ts create mode 100644 prisma/content/courses/csharp-poo-1/units/14-concurrencia/lessons/03-estado-compartido.ts create mode 100644 prisma/content/courses/csharp-poo-1/units/14-concurrencia/lessons/04-lock-y-exclusion-mutua.ts create mode 100644 prisma/content/courses/csharp-poo-1/units/14-concurrencia/lessons/05-errores-en-hilos.ts create mode 100644 prisma/content/courses/csharp-poo-1/units/14-concurrencia/practice.ts create mode 100644 prisma/content/courses/csharp-poo-1/units/15-redes/index.ts create mode 100644 prisma/content/courses/csharp-poo-1/units/15-redes/lessons/01-protocolo-mensaje-socket.ts create mode 100644 prisma/content/courses/csharp-poo-1/units/15-redes/lessons/02-tcp-y-udp.ts create mode 100644 prisma/content/courses/csharp-poo-1/units/15-redes/lessons/03-ip-puerto-endpoint.ts create mode 100644 prisma/content/courses/csharp-poo-1/units/15-redes/lessons/04-cliente-servidor-tcp.ts create mode 100644 prisma/content/courses/csharp-poo-1/units/15-redes/lessons/05-udp-y-mensajes.ts create mode 100644 prisma/content/courses/csharp-poo-1/units/15-redes/lessons/06-seguridad-del-mensaje.ts create mode 100644 prisma/content/courses/csharp-poo-1/units/15-redes/practice.ts create mode 100644 prisma/content/courses/csharp-poo-1/units/16-integrador/index.ts create mode 100644 prisma/content/courses/csharp-poo-1/units/16-integrador/lessons/01-requisitos-y-uml.ts create mode 100644 prisma/content/courses/csharp-poo-1/units/16-integrador/lessons/02-arquitectura-del-proyecto.ts create mode 100644 prisma/content/courses/csharp-poo-1/units/16-integrador/lessons/03-integracion-concurrente.ts create mode 100644 prisma/content/courses/csharp-poo-1/units/16-integrador/lessons/04-entrega-y-evidencias.ts create mode 100644 prisma/content/courses/csharp-poo-1/units/16-integrador/practice.ts diff --git a/prisma/content/courses/csharp-poo-1/index.ts b/prisma/content/courses/csharp-poo-1/index.ts index 07b19f8..865236c 100644 --- a/prisma/content/courses/csharp-poo-1/index.ts +++ b/prisma/content/courses/csharp-poo-1/index.ts @@ -1,49 +1,61 @@ -// ===================================================================== -// Paquete de curso: Programación Orientada a Objetos I con C#. -// -// Legacy: las 8 unidades y los 8 sets de práctica siguen viviendo en -// `prisma/content/csharp/unidad-*.ts` y -// `prisma/content/exercises/csharp/u*.ts` — NO se movieron ni se -// editaron. Aquí sólo se reutilizan y se pasan por `adaptLegacyUnits` + -// `defineCourse` para entrar a la misma capa de authoring que los cursos -// nuevos. -// -// Segundo caso de `curriculum`, deliberadamente distinto de C++: UNA sola -// sección (3.er semestre) que envuelve las 8 unidades existentes tal -// cual, en su mismo orden. Demuestra que la agrupación curricular no está -// hardcodeada para C++ ni para dos semestres. -// ===================================================================== - import { adaptLegacyUnits, defineCourse } from "../../authoring"; import { cursoCsharpPoo1 } from "../../csharp"; import { csharpPracticeSets } from "../../exercises/csharp"; -const authoredUnits = adaptLegacyUnits(cursoCsharpPoo1, csharpPracticeSets); +import { csharpPoo2Colecciones } from "./units/09-colecciones"; +import { csharpPoo2Diccionarios } from "./units/10-diccionarios"; +import { csharpPoo2Ordenamiento } from "./units/11-ordenamiento"; +import { csharpPoo2Xml } from "./units/12-xml"; +import { csharpPoo2Genericos } from "./units/13-genericos"; +import { csharpPoo2Concurrencia } from "./units/14-concurrencia"; +import { csharpPoo2Redes } from "./units/15-redes"; +import { csharpPoo2Integrador } from "./units/16-integrador"; + +// ===================================================================== +// Paquete de curso: Programación Orientada a Objetos con C#. +// +// S3 (legacy): las 8 unidades y los 8 sets de práctica de POO I siguen +// viviendo en `prisma/content/csharp/unidad-*.ts` y +// `prisma/content/exercises/csharp/u*.ts` — NO se movieron ni se editaron. +// Aquí sólo se reutilizan vía `adaptLegacyUnits`. +// +// S4 (nueva, POO II): capa de authoring completa, colocalizada por unidad +// bajo `./units/`. Las 8 unidades entran con `published: false` — el +// release gate de S4 es una tarea posterior. +// ===================================================================== -const { - slug, - title, - description, - subjectName, - academicContext, - language, - executionProfile, -} = cursoCsharpPoo1; +const authoredUnitsS3 = adaptLegacyUnits(cursoCsharpPoo1, csharpPracticeSets); export const csharpPoo1 = defineCourse({ - slug, - title, - description, - subjectName, - academicContext, - language, - executionProfile, + slug: "csharp-poo-1", + title: "Programación Orientada a Objetos con C#", + description: + "Modela, implementa y conecta aplicaciones orientadas a objetos en C#, desde clases y relaciones hasta colecciones, persistencia, concurrencia y redes.", + subjectName: "Programación Orientada a Objetos", + academicContext: "CETI · Tecnólogo en Desarrollo de Software", + language: cursoCsharpPoo1.language, + executionProfile: cursoCsharpPoo1.executionProfile, curriculum: [ { key: "s3-programacion-orientada-objetos-1", semester: 3, subjectName: "Programación Orientada a Objetos I", - units: authoredUnits, + units: authoredUnitsS3, + }, + { + key: "s4-programacion-orientada-objetos-2", + semester: 4, + subjectName: "Programación Orientada a Objetos II", + units: [ + csharpPoo2Colecciones, + csharpPoo2Diccionarios, + csharpPoo2Ordenamiento, + csharpPoo2Xml, + csharpPoo2Genericos, + csharpPoo2Concurrencia, + csharpPoo2Redes, + csharpPoo2Integrador, + ], }, ], }); diff --git a/prisma/content/courses/csharp-poo-1/units/09-colecciones/index.ts b/prisma/content/courses/csharp-poo-1/units/09-colecciones/index.ts new file mode 100644 index 0000000..6e9a283 --- /dev/null +++ b/prisma/content/courses/csharp-poo-1/units/09-colecciones/index.ts @@ -0,0 +1,20 @@ +import { defineUnit } from "../../../../authoring"; + +import { leccion01 } from "./lessons/01-de-arreglos-a-colecciones"; +import { leccion02 } from "./lessons/02-list-de-objetos"; +import { leccion03 } from "./lessons/03-stack-y-queue"; +import { leccion04 } from "./lessons/04-agregar-buscar-eliminar"; +import { leccion05 } from "./lessons/05-inventario-en-memoria"; + +import { practice } from "./practice"; + +export const csharpPoo2Colecciones = defineUnit({ + slug: "csharp-poo2-01-colecciones", + title: "Colecciones y estructuras lineales", + description: + "Sustituye arreglos de tamaño fijo por colecciones dinámicas y elige entre lista, pila y cola según la operación del problema.", + icon: "📚", + published: false, + lessons: [leccion01, leccion02, leccion03, leccion04, leccion05], + practice, +}); diff --git a/prisma/content/courses/csharp-poo-1/units/09-colecciones/lessons/01-de-arreglos-a-colecciones.ts b/prisma/content/courses/csharp-poo-1/units/09-colecciones/lessons/01-de-arreglos-a-colecciones.ts new file mode 100644 index 0000000..71fb7b7 --- /dev/null +++ b/prisma/content/courses/csharp-poo-1/units/09-colecciones/lessons/01-de-arreglos-a-colecciones.ts @@ -0,0 +1,139 @@ +import { defineLesson } from "../../../../../authoring"; + +export const leccion01 = defineLesson({ + slug: "de-arreglos-a-colecciones", + title: "De arreglos fijos a colecciones", + description: + "Reconoce cuándo el tamaño fijo de un arreglo deja de representar bien un conjunto de objetos.", + estimatedMinutes: 13, + xpReward: 45, + steps: [ + { + type: "theory", + markdown: `# El problema no es el arreglo: es conocer el tamaño de antemano + +Un arreglo funciona muy bien cuando la cantidad de elementos está definida desde el inicio. Si declaras \`Producto[] productos = new Producto[20]\`, reservaste exactamente veinte lugares. + +En un inventario real normalmente no sabes cuántos productos existirán mañana. Puedes llenar el arreglo, dejar huecos o copiar todo a uno más grande, pero entonces empiezas a programar manualmente el crecimiento de una estructura que debería ayudarte a resolver el problema. + +Una **colección dinámica** separa dos ideas: + +- cuántos elementos existen ahora; +- cuánta memoria necesita internamente para guardarlos. + +En C#, \`List\` representa una secuencia cuyo tamaño puede crecer o disminuir. \`T\` indica qué tipo de elementos admite: \`List\`, \`List\`, \`List\`. + +No reemplaza a todos los arreglos. Si el tamaño es fijo por naturaleza, un arreglo sigue siendo una opción válida. La elección depende del problema.`, + }, + { + type: "code_example", + code: `using System; +using System.Collections.Generic; + +class Program +{ + static void Main() + { + List productos = new List(); + + productos.Add("Mouse"); + productos.Add("Teclado"); + productos.Add("Monitor"); + + Console.WriteLine("Cantidad: " + productos.Count); + Console.WriteLine(productos[1]); + } +}`, + explanation: + "`Add` incorpora un elemento sin que el programa tenga que calcular un nuevo tamaño. `Count` es la cantidad real de elementos y el acceso por índice conserva la idea conocida de los arreglos.", + runnable: true, + expectedOutput: `Cantidad: 3 +Teclado`, + }, + { + type: "matching", + pairs: [ + { left: "Producto[30]", right: "Cantidad máxima conocida de antemano." }, + { + left: "List", + right: "Cantidad de objetos que puede crecer o disminuir.", + }, + { left: "Count", right: "Cantidad actual de elementos." }, + { left: "Add", right: "Agrega un elemento al final." }, + ], + explanation: + "Una colección dinámica no elimina los índices; elimina la obligación de fijar el tamaño útil antes de conocer los datos.", + }, + { + type: "code_challenge", + exercise: { + prompt: + "Lee `n`, después `n` enteros. Guarda todos los valores en un `List`. Al final imprime primero `Cantidad: X` y después `Suma: Y`.\n\nNo calcules la cantidad con una variable paralela: usa `Count` de la lista.", + starterCode: `using System; +using System.Collections.Generic; + +class Program +{ + static void Main() + { + int n = int.Parse(Console.ReadLine()); + List valores = new List(); + + // agrega los n valores + + // imprime Count y la suma + } +}`, + solutionCode: `using System; +using System.Collections.Generic; + +class Program +{ + static void Main() + { + int n = int.Parse(Console.ReadLine()); + List valores = new List(); + + for (int i = 0; i < n; i++) + { + valores.Add(int.Parse(Console.ReadLine())); + } + + int suma = 0; + for (int i = 0; i < valores.Count; i++) + { + suma += valores[i]; + } + + Console.WriteLine("Cantidad: " + valores.Count); + Console.WriteLine("Suma: " + suma); + } +}`, + hints: [ + "Crea `new List()` una sola vez.", + "Usa `Add` dentro del primer `for`.", + "El segundo recorrido debe terminar en `valores.Count`.", + ], + difficulty: "easy", + xpReward: 24, + testCases: [ + { + visible: true, + stdin: "3\n4\n5\n6\n", + expectedStdout: "Cantidad: 3\nSuma: 15\n", + }, + { + visible: false, + stdin: "1\n-7\n", + expectedStdout: "Cantidad: 1\nSuma: -7\n", + }, + { + visible: false, + stdin: "5\n0\n2\n0\n8\n-3\n", + expectedStdout: "Cantidad: 5\nSuma: 7\n", + }, + ], + }, + }, + ], +}); diff --git a/prisma/content/courses/csharp-poo-1/units/09-colecciones/lessons/02-list-de-objetos.ts b/prisma/content/courses/csharp-poo-1/units/09-colecciones/lessons/02-list-de-objetos.ts new file mode 100644 index 0000000..e97ad0f --- /dev/null +++ b/prisma/content/courses/csharp-poo-1/units/09-colecciones/lessons/02-list-de-objetos.ts @@ -0,0 +1,171 @@ +import { defineLesson } from "../../../../../authoring"; + +export const leccion02 = defineLesson({ + slug: "list-de-objetos", + title: "Listas de objetos", + description: + "Guarda objetos completos en una lista y recorre sus propiedades sin perder el modelo orientado a objetos.", + estimatedMinutes: 15, + xpReward: 50, + steps: [ + { + type: "theory", + markdown: `# Una colección organiza objetos, no reemplaza sus responsabilidades + +\`List\` significa que cada posición guarda una referencia a un objeto \`Producto\`. La colección sabe **organizar** la secuencia; \`Producto\` sigue siendo responsable de representar un producto válido. + +Esto evita un error común: mantener arreglos paralelos como \`nombres[]\`, \`precios[]\` y \`stocks[]\`. Con objetos, los datos que pertenecen a la misma entidad viajan juntos. + +La lista ofrece operaciones de colección (\`Add\`, \`Remove\`, \`Count\`, índice). Las reglas del dominio siguen dentro de las clases.`, + }, + { + type: "code_example", + code: `using System; +using System.Collections.Generic; + +class Producto +{ + public string Nombre { get; private set; } + public int Stock { get; private set; } + + public Producto(string nombre, int stock) + { + Nombre = nombre; + Stock = stock; + } +} + +class Program +{ + static void Main() + { + List productos = new List(); + productos.Add(new Producto("Mouse", 4)); + productos.Add(new Producto("Teclado", 7)); + + for (int i = 0; i < productos.Count; i++) + { + Console.WriteLine(productos[i].Nombre + ": " + productos[i].Stock); + } + } +}`, + explanation: + "cada elemento de la lista es un `Producto`. El recorrido obtiene una entidad completa y luego consulta sus propiedades.", + runnable: true, + expectedOutput: `Mouse: 4 +Teclado: 7`, + }, + { + type: "fill_blank", + prompt: + "completa el tipo genérico, la creación de la lista y la operación de alta.", + template: `List<{{0}}> productos = new {{1}}(); +Producto p = new Producto("Cable", 5); +productos.{{2}}(p);`, + blanks: [ + { answer: "Producto" }, + { answer: "List" }, + { answer: "Add" }, + ], + explanation: + "el tipo escrito entre `< >` restringe la colección a objetos compatibles con `Producto`.", + }, + { + type: "code_challenge", + exercise: { + prompt: + "Crea una clase `Producto` con propiedades públicas `Nombre` (`string`) y `Stock` (`int`) con `private set`, inicializadas por constructor.\n\nLee `n`. Para cada producto lee una línea con el nombre y otra con el stock, crea el objeto y agrégalo a un `List`. Imprime:\n\n- `Productos: N`\n- `Stock total: S`", + starterCode: `using System; +using System.Collections.Generic; + +class Producto +{ + // propiedades y constructor +} + +class Program +{ + static void Main() + { + // lee, crea, agrega y resume + } +}`, + solutionCode: `using System; +using System.Collections.Generic; + +class Producto +{ + public string Nombre { get; private set; } + public int Stock { get; private set; } + + public Producto(string nombre, int stock) + { + Nombre = nombre; + Stock = stock; + } +} + +class Program +{ + static void Main() + { + int n = int.Parse(Console.ReadLine()); + List productos = new List(); + + for (int i = 0; i < n; i++) + { + string nombre = Console.ReadLine(); + int stock = int.Parse(Console.ReadLine()); + productos.Add(new Producto(nombre, stock)); + } + + int total = 0; + for (int i = 0; i < productos.Count; i++) + { + total += productos[i].Stock; + } + + Console.WriteLine("Productos: " + productos.Count); + Console.WriteLine("Stock total: " + total); + } +}`, + hints: [ + "La lista debe ser `List`.", + "Agrega un objeto nuevo en cada iteración.", + "Suma la propiedad `Stock`, no el índice.", + ], + difficulty: "medium", + xpReward: 30, + structure: { + classes: [ + { + name: "Producto", + properties: [ + { name: "Nombre", visibility: "public", type: "string" }, + { name: "Stock", visibility: "public", type: "int" }, + ], + constructors: [{ paramCount: 2 }], + }, + ], + }, + testCases: [ + { + visible: true, + stdin: "2\nMouse\n4\nTeclado\n7\n", + expectedStdout: "Productos: 2\nStock total: 11\n", + }, + { + visible: false, + stdin: "1\nCable HDMI\n0\n", + expectedStdout: "Productos: 1\nStock total: 0\n", + }, + { + visible: false, + stdin: "3\nA\n2\nB\n5\nC\n9\n", + expectedStdout: "Productos: 3\nStock total: 16\n", + }, + ], + }, + }, + ], +}); diff --git a/prisma/content/courses/csharp-poo-1/units/09-colecciones/lessons/03-stack-y-queue.ts b/prisma/content/courses/csharp-poo-1/units/09-colecciones/lessons/03-stack-y-queue.ts new file mode 100644 index 0000000..172fe6f --- /dev/null +++ b/prisma/content/courses/csharp-poo-1/units/09-colecciones/lessons/03-stack-y-queue.ts @@ -0,0 +1,132 @@ +import { defineLesson } from "../../../../../authoring"; + +export const leccion03 = defineLesson({ + slug: "stack-y-queue", + title: "Pila y cola: el orden también es una regla", + description: + "Distingue LIFO y FIFO y usa la estructura cuya política de salida coincide con el problema.", + estimatedMinutes: 14, + xpReward: 50, + steps: [ + { + type: "theory", + markdown: `# No todas las secuencias deben salir por el mismo lado + +Una \`List\` te deja acceder por índice. A veces eso es demasiado poder: el problema ya define qué elemento debe salir primero. + +- **Pila (\`Stack\`)**: último en entrar, primero en salir (**LIFO**). Ejemplos: deshacer acciones, regresar por un historial, evaluar expresiones. +- **Cola (\`Queue\`)**: primero en entrar, primero en salir (**FIFO**). Ejemplos: turnos, trabajos pendientes, solicitudes. + +Las operaciones hacen explícita la regla: + +- pila: \`Push\`, \`Pop\`, \`Peek\`; +- cola: \`Enqueue\`, \`Dequeue\`, \`Peek\`. + +Elegir la estructura correcta hace que el código explique el comportamiento del sistema.`, + }, + { + type: "code_example", + code: `using System; +using System.Collections.Generic; + +class Program +{ + static void Main() + { + Stack historial = new Stack(); + historial.Push("abrir"); + historial.Push("editar"); + historial.Push("guardar"); + Console.WriteLine("Deshacer: " + historial.Pop()); + + Queue turnos = new Queue(); + turnos.Enqueue("Ana"); + turnos.Enqueue("Luis"); + Console.WriteLine("Atender: " + turnos.Dequeue()); + } +}`, + explanation: + "la pila devuelve `guardar`, que fue lo último agregado. La cola devuelve `Ana`, que llegó primero.", + runnable: true, + expectedOutput: `Deshacer: guardar +Atender: Ana`, + }, + { + type: "matching", + pairs: [ + { left: "Historial de “deshacer”", right: "Stack / LIFO" }, + { left: "Fila de atención", right: "Queue / FIFO" }, + { left: "Agregar a pila", right: "Push" }, + { left: "Agregar a cola", right: "Enqueue" }, + { left: "Consultar sin retirar", right: "Peek" }, + ], + }, + { + type: "code_challenge", + exercise: { + prompt: + "Lee `n` nombres y colócalos en una `Queue` en el orden en que llegan. Después atiende toda la cola: imprime un nombre por línea usando `Dequeue` hasta que quede vacía.", + starterCode: `using System; +using System.Collections.Generic; + +class Program +{ + static void Main() + { + int n = int.Parse(Console.ReadLine()); + Queue turnos = new Queue(); + + // encolar + + // atender + } +}`, + solutionCode: `using System; +using System.Collections.Generic; + +class Program +{ + static void Main() + { + int n = int.Parse(Console.ReadLine()); + Queue turnos = new Queue(); + + for (int i = 0; i < n; i++) + { + turnos.Enqueue(Console.ReadLine()); + } + + while (turnos.Count > 0) + { + Console.WriteLine(turnos.Dequeue()); + } + } +}`, + hints: [ + "`Enqueue` agrega al final de la fila.", + "`Count > 0` indica que todavía hay alguien esperando.", + "`Dequeue` devuelve y elimina al primero.", + ], + difficulty: "easy", + xpReward: 24, + testCases: [ + { + visible: true, + stdin: "3\nAna\nLuis\nMara\n", + expectedStdout: "Ana\nLuis\nMara\n", + }, + { + visible: false, + stdin: "1\nSolo\n", + expectedStdout: "Solo\n", + }, + { + visible: false, + stdin: "4\nA\nB\nC\nD\n", + expectedStdout: "A\nB\nC\nD\n", + }, + ], + }, + }, + ], +}); diff --git a/prisma/content/courses/csharp-poo-1/units/09-colecciones/lessons/04-agregar-buscar-eliminar.ts b/prisma/content/courses/csharp-poo-1/units/09-colecciones/lessons/04-agregar-buscar-eliminar.ts new file mode 100644 index 0000000..67d6fd1 --- /dev/null +++ b/prisma/content/courses/csharp-poo-1/units/09-colecciones/lessons/04-agregar-buscar-eliminar.ts @@ -0,0 +1,184 @@ +import { defineLesson } from "../../../../../authoring"; + +export const leccion04 = defineLesson({ + slug: "agregar-buscar-eliminar", + title: "Agregar, buscar y eliminar objetos", + description: + "Implementa las operaciones básicas de una estructura sin confundir posición con identidad del objeto.", + estimatedMinutes: 16, + xpReward: 55, + steps: [ + { + type: "theory", + markdown: `# El índice no es la identidad + +Si un producto tiene código \`P-17\`, ese código identifica al producto en el dominio. Que hoy esté en la posición 3 de una lista es sólo un detalle de organización. + +Por eso las operaciones típicas se expresan así: + +1. **agregar** un objeto; +2. **buscar** por una clave del dominio; +3. **eliminar** el objeto encontrado. + +Con una lista pequeña puedes recorrer secuencialmente y comparar la propiedad \`Codigo\`. Más adelante un diccionario permitirá acceder directamente por clave; primero conviene dominar la operación independientemente de la estructura concreta.`, + }, + { + type: "code_example", + code: `using System; +using System.Collections.Generic; + +class Producto +{ + public string Codigo { get; private set; } + public Producto(string codigo) { Codigo = codigo; } +} + +class Program +{ + static void Main() + { + List productos = new List(); + productos.Add(new Producto("A1")); + productos.Add(new Producto("B2")); + productos.Add(new Producto("C3")); + + string buscado = "B2"; + Producto encontrado = null; + + for (int i = 0; i < productos.Count; i++) + { + if (productos[i].Codigo == buscado) + { + encontrado = productos[i]; + break; + } + } + + if (encontrado != null) + { + productos.Remove(encontrado); + } + + Console.WriteLine(productos.Count); + } +}`, + explanation: + "el programa busca por `Codigo`, conserva la referencia encontrada y pide a la lista que elimine ese objeto. No depende de que `B2` esté siempre en la posición 1.", + runnable: true, + expectedOutput: "2", + }, + { + type: "quiz", + question: + "si eliminas elementos de una lista dentro de un `for` ascendente usando `RemoveAt(i)`, ¿qué riesgo aparece?", + options: [ + "Ninguno: los índices nunca cambian.", + "El siguiente elemento se desplaza al índice actual y puede quedar sin revisar.", + "`Count` aumenta automáticamente.", + "C# convierte la lista en arreglo.", + ], + correctIndex: 1, + explanation: + "al retirar un elemento, los posteriores se desplazan. Por eso al filtrar por índice suele recorrerse de atrás hacia adelante o separarse la búsqueda de la eliminación.", + }, + { + type: "code_challenge", + exercise: { + prompt: + "Crea `Producto` con `Codigo` y `Stock`. Lee `n`, luego `n` pares de líneas (`codigo`, `stock`). Después lee un código a eliminar.\n\nBusca ese producto por `Codigo` y elimínalo si existe. Imprime primero `Restantes: N` y luego los códigos restantes, uno por línea, conservando su orden original.", + starterCode: `using System; +using System.Collections.Generic; + +class Producto +{ + // Codigo, Stock y constructor +} + +class Program +{ + static void Main() + { + // carga, busca y elimina + } +}`, + solutionCode: `using System; +using System.Collections.Generic; + +class Producto +{ + public string Codigo { get; private set; } + public int Stock { get; private set; } + + public Producto(string codigo, int stock) + { + Codigo = codigo; + Stock = stock; + } +} + +class Program +{ + static void Main() + { + int n = int.Parse(Console.ReadLine()); + List productos = new List(); + + for (int i = 0; i < n; i++) + { + string codigo = Console.ReadLine(); + int stock = int.Parse(Console.ReadLine()); + productos.Add(new Producto(codigo, stock)); + } + + string objetivo = Console.ReadLine(); + Producto encontrado = null; + + for (int i = 0; i < productos.Count; i++) + { + if (productos[i].Codigo == objetivo) + { + encontrado = productos[i]; + break; + } + } + + if (encontrado != null) + { + productos.Remove(encontrado); + } + + Console.WriteLine("Restantes: " + productos.Count); + for (int i = 0; i < productos.Count; i++) + { + Console.WriteLine(productos[i].Codigo); + } + } +}`, + hints: [ + "Busca por `Producto.Codigo`, no por un índice fijo.", + "Guarda el objeto encontrado en una variable que puede ser `null`.", + "Llama `Remove` sólo cuando realmente lo encontraste.", + ], + difficulty: "medium", + xpReward: 32, + testCases: [ + { + visible: true, + stdin: "3\nA1\n2\nB2\n4\nC3\n1\nB2\n", + expectedStdout: "Restantes: 2\nA1\nC3\n", + }, + { + visible: false, + stdin: "2\nX\n0\nY\n9\nZ\n", + expectedStdout: "Restantes: 2\nX\nY\n", + }, + { + visible: false, + stdin: "1\nP7\n5\nP7\n", + expectedStdout: "Restantes: 0\n", + }, + ], + }, + }, + ], +}); diff --git a/prisma/content/courses/csharp-poo-1/units/09-colecciones/lessons/05-inventario-en-memoria.ts b/prisma/content/courses/csharp-poo-1/units/09-colecciones/lessons/05-inventario-en-memoria.ts new file mode 100644 index 0000000..53e7d72 --- /dev/null +++ b/prisma/content/courses/csharp-poo-1/units/09-colecciones/lessons/05-inventario-en-memoria.ts @@ -0,0 +1,215 @@ +import { defineLesson } from "../../../../../authoring"; + +export const leccion05 = defineLesson({ + slug: "inventario-en-memoria", + title: "Un inventario en memoria", + description: + "Encapsula una colección dentro de una clase de servicio y expone operaciones del dominio en lugar de entregar la lista al resto del programa.", + estimatedMinutes: 18, + xpReward: 60, + steps: [ + { + type: "theory", + markdown: `# La colección es un detalle interno + +Si todo \`Program\` recibe acceso directo a \`List\`, cualquier parte puede agregar, borrar o reemplazar objetos sin respetar reglas. + +Una clase \`Inventario\` puede **poseer** la colección y publicar operaciones con intención: + +- \`Agregar(Producto)\`; +- \`Buscar(string codigo)\`; +- \`Eliminar(string codigo)\`. + +Eso aplica composición: el inventario *tiene* una colección. También deja preparado el diseño para cambiar la estructura interna después sin obligar a todos los consumidores a reescribirse.`, + }, + { + type: "code_example", + code: `using System; +using System.Collections.Generic; + +class Producto +{ + public string Codigo { get; private set; } + public int Stock { get; private set; } + public Producto(string codigo, int stock) + { + Codigo = codigo; + Stock = stock; + } +} + +class Inventario +{ + private List productos = new List(); + + public void Agregar(Producto producto) + { + productos.Add(producto); + } + + public Producto Buscar(string codigo) + { + for (int i = 0; i < productos.Count; i++) + { + if (productos[i].Codigo == codigo) return productos[i]; + } + return null; + } +} + +class Program +{ + static void Main() + { + Inventario inventario = new Inventario(); + inventario.Agregar(new Producto("P1", 8)); + Producto p = inventario.Buscar("P1"); + Console.WriteLine(p == null ? "NO" : p.Stock.ToString()); + } +}`, + explanation: + "`Program` no necesita saber si el inventario usa una lista, arreglo o diccionario. Sólo conoce las operaciones públicas del objeto.", + runnable: true, + expectedOutput: "8", + }, + { + type: "matching", + pairs: [ + { left: "Inventario", right: "Responsable de organizar/buscar productos." }, + { left: "Producto", right: "Responsable de representar un producto." }, + { left: "List privada", right: "Detalle interno de almacenamiento." }, + { left: "Buscar(codigo)", right: "Operación del dominio expuesta al consumidor." }, + ], + }, + { + type: "code_challenge", + exercise: { + prompt: + "Implementa `Producto` (`Codigo`, `Stock`) e `Inventario` con una `List` privada y métodos `Agregar`, `Buscar` y `Eliminar`.\n\nEl programa recibe `n` comandos:\n\n- `A CODIGO STOCK` agrega un producto;\n- `B CODIGO` busca e imprime su stock, o `NO` si no existe;\n- `E CODIGO` elimina si existe y no imprime nada.\n\nLos códigos no contienen espacios.", + starterCode: `using System; +using System.Collections.Generic; + +class Producto { } +class Inventario { } + +class Program +{ + static void Main() + { + // procesa comandos + } +}`, + solutionCode: `using System; +using System.Collections.Generic; + +class Producto +{ + public string Codigo { get; private set; } + public int Stock { get; private set; } + public Producto(string codigo, int stock) + { + Codigo = codigo; + Stock = stock; + } +} + +class Inventario +{ + private List productos = new List(); + + public void Agregar(Producto producto) + { + productos.Add(producto); + } + + public Producto Buscar(string codigo) + { + for (int i = 0; i < productos.Count; i++) + { + if (productos[i].Codigo == codigo) return productos[i]; + } + return null; + } + + public void Eliminar(string codigo) + { + Producto producto = Buscar(codigo); + if (producto != null) productos.Remove(producto); + } +} + +class Program +{ + static void Main() + { + int n = int.Parse(Console.ReadLine()); + Inventario inventario = new Inventario(); + + for (int i = 0; i < n; i++) + { + string[] partes = Console.ReadLine().Split(' '); + if (partes[0] == "A") + { + inventario.Agregar(new Producto(partes[1], int.Parse(partes[2]))); + } + else if (partes[0] == "B") + { + Producto producto = inventario.Buscar(partes[1]); + Console.WriteLine(producto == null ? "NO" : producto.Stock.ToString()); + } + else if (partes[0] == "E") + { + inventario.Eliminar(partes[1]); + } + } + } +}`, + hints: [ + "La lista pertenece a `Inventario`, no a `Program`.", + "`Eliminar` puede reutilizar `Buscar`.", + "`Split(' ')` separa el comando de sus argumentos.", + ], + difficulty: "hard", + xpReward: 40, + structure: { + classes: [ + { + name: "Producto", + properties: [ + { name: "Codigo", visibility: "public", type: "string" }, + { name: "Stock", visibility: "public", type: "int" }, + ], + constructors: [{ paramCount: 2 }], + }, + { + name: "Inventario", + stores: [{ type: "Producto" }], + methods: [ + { name: "Agregar", visibility: "public", paramCount: 1 }, + { name: "Buscar", visibility: "public", paramCount: 1 }, + { name: "Eliminar", visibility: "public", paramCount: 1 }, + ], + }, + ], + }, + testCases: [ + { + visible: true, + stdin: "5\nA P1 8\nA P2 3\nB P2\nE P2\nB P2\n", + expectedStdout: "3\nNO\n", + }, + { + visible: false, + stdin: "4\nB X\nA X 0\nB X\nE X\n", + expectedStdout: "NO\n0\n", + }, + { + visible: false, + stdin: "7\nA A 1\nA B 2\nA C 3\nE B\nB A\nB B\nB C\n", + expectedStdout: "1\nNO\n3\n", + }, + ], + }, + }, + ], +}); diff --git a/prisma/content/courses/csharp-poo-1/units/09-colecciones/practice.ts b/prisma/content/courses/csharp-poo-1/units/09-colecciones/practice.ts new file mode 100644 index 0000000..5007c4f --- /dev/null +++ b/prisma/content/courses/csharp-poo-1/units/09-colecciones/practice.ts @@ -0,0 +1,202 @@ +import type { PracticeExerciseDefinition } from "../../../../exercises/types"; + +export const practice = [ + { + slug: "csharp-poo2-lista-productos", + title: "Inventario dinámico", + description: + "Carga objetos en una lista sin tamaño máximo fijado por el programa.", + prompt: + "crea `Producto(Nombre,Stock)`. Lee `n` productos (nombre y stock en líneas separadas), guárdalos en `List` e imprime `N productos` y `S unidades`.", + starterCode: `using System; +using System.Collections.Generic; +class Producto { } +class Program { static void Main() { } }`, + solutionCode: `using System; +using System.Collections.Generic; +class Producto +{ + public string Nombre { get; private set; } + public int Stock { get; private set; } + public Producto(string nombre, int stock) { Nombre = nombre; Stock = stock; } +} +class Program +{ + static void Main() + { + int n = int.Parse(Console.ReadLine()); + List productos = new List(); + for (int i = 0; i < n; i++) + productos.Add(new Producto(Console.ReadLine(), int.Parse(Console.ReadLine()))); + int stock = 0; + for (int i = 0; i < productos.Count; i++) stock += productos[i].Stock; + Console.WriteLine(productos.Count + " productos"); + Console.WriteLine(stock + " unidades"); + } +}`, + hints: ["`List`, `Add`, `Count`."], + difficulty: "easy", + xpReward: 24, + testCases: [ + { + stdin: "2\nMouse\n3\nCable\n7\n", + expectedStdout: "2 productos\n10 unidades\n", + visible: true, + }, + { + stdin: "1\nX\n0\n", + expectedStdout: "1 productos\n0 unidades\n", + visible: false, + }, + { + stdin: "4\nA\n1\nB\n2\nC\n3\nD\n4\n", + expectedStdout: "4 productos\n10 unidades\n", + visible: false, + }, + ], + }, + { + slug: "csharp-poo2-eliminar-agotados", + title: "Retira agotados", + description: + "Elimina varios objetos de una lista sin saltarte elementos al recorrerla.", + prompt: + "lee `n` productos (`nombre`, `stock`). Elimina todos los que tengan stock `0`, recorriendo de forma segura. Imprime los nombres restantes, uno por línea; si no queda ninguno, imprime `VACIO`.", + starterCode: `using System; +using System.Collections.Generic; +class Producto { } +class Program { static void Main() { } }`, + solutionCode: `using System; +using System.Collections.Generic; +class Producto +{ + public string Nombre { get; private set; } + public int Stock { get; private set; } + public Producto(string nombre, int stock) { Nombre = nombre; Stock = stock; } +} +class Program +{ + static void Main() + { + int n = int.Parse(Console.ReadLine()); + List productos = new List(); + for (int i = 0; i < n; i++) + productos.Add(new Producto(Console.ReadLine(), int.Parse(Console.ReadLine()))); + for (int i = productos.Count - 1; i >= 0; i--) + if (productos[i].Stock == 0) productos.RemoveAt(i); + if (productos.Count == 0) Console.WriteLine("VACIO"); + else for (int i = 0; i < productos.Count; i++) Console.WriteLine(productos[i].Nombre); + } +}`, + hints: ["al borrar por índice, recorre de `Count - 1` hacia `0`."], + difficulty: "medium", + xpReward: 30, + testCases: [ + { + stdin: "3\nA\n0\nB\n2\nC\n0\n", + expectedStdout: "B\n", + visible: true, + }, + { + stdin: "2\nA\n0\nB\n0\n", + expectedStdout: "VACIO\n", + visible: false, + }, + { + stdin: "3\nA\n1\nB\n2\nC\n3\n", + expectedStdout: "A\nB\nC\n", + visible: false, + }, + ], + }, + { + slug: "csharp-poo2-pila-historial", + title: "Historial de acciones", + description: "Usa una pila para reproducir un historial desde la acción más reciente.", + prompt: + "lee `n` acciones y apílalas. Después imprime todas usando `Pop`, de la más reciente a la más antigua.", + starterCode: `using System; +using System.Collections.Generic; +class Program { static void Main() { } }`, + solutionCode: `using System; +using System.Collections.Generic; +class Program +{ + static void Main() + { + int n = int.Parse(Console.ReadLine()); + Stack historial = new Stack(); + for (int i = 0; i < n; i++) historial.Push(Console.ReadLine()); + while (historial.Count > 0) Console.WriteLine(historial.Pop()); + } +}`, + difficulty: "easy", + xpReward: 24, + testCases: [ + { + stdin: "3\nabrir\neditar\nguardar\n", + expectedStdout: "guardar\neditar\nabrir\n", + visible: true, + }, + { + stdin: "1\nlogin\n", + expectedStdout: "login\n", + visible: false, + }, + { + stdin: "4\nA\nB\nC\nD\n", + expectedStdout: "D\nC\nB\nA\n", + visible: false, + }, + ], + }, + { + slug: "csharp-poo2-cola-turnos", + title: "Ventanilla de turnos", + description: + "Modela atención FIFO y deja pendientes los turnos que todavía no alcanzan a procesarse.", + prompt: + "lee `n` nombres, encola todos y luego lee `k`. Atiende como máximo `k` personas. Por cada atención imprime `Atendido: NOMBRE`. Al final imprime `Pendientes: X`.", + starterCode: `using System; +using System.Collections.Generic; +class Program { static void Main() { } }`, + solutionCode: `using System; +using System.Collections.Generic; +class Program +{ + static void Main() + { + int n = int.Parse(Console.ReadLine()); + Queue turnos = new Queue(); + for (int i = 0; i < n; i++) turnos.Enqueue(Console.ReadLine()); + int k = int.Parse(Console.ReadLine()); + int atendidos = 0; + while (turnos.Count > 0 && atendidos < k) + { + Console.WriteLine("Atendido: " + turnos.Dequeue()); + atendidos++; + } + Console.WriteLine("Pendientes: " + turnos.Count); + } +}`, + difficulty: "medium", + xpReward: 30, + testCases: [ + { + stdin: "3\nAna\nBeto\nCaro\n2\n", + expectedStdout: "Atendido: Ana\nAtendido: Beto\nPendientes: 1\n", + visible: true, + }, + { + stdin: "2\nA\nB\n5\n", + expectedStdout: "Atendido: A\nAtendido: B\nPendientes: 0\n", + visible: false, + }, + { + stdin: "2\nA\nB\n0\n", + expectedStdout: "Pendientes: 2\n", + visible: false, + }, + ], + }, +] satisfies PracticeExerciseDefinition[]; diff --git a/prisma/content/courses/csharp-poo-1/units/10-diccionarios/index.ts b/prisma/content/courses/csharp-poo-1/units/10-diccionarios/index.ts new file mode 100644 index 0000000..3922b5e --- /dev/null +++ b/prisma/content/courses/csharp-poo-1/units/10-diccionarios/index.ts @@ -0,0 +1,20 @@ +import { defineUnit } from "../../../../authoring"; + +import { leccion01 } from "./lessons/01-clave-y-valor"; +import { leccion02 } from "./lessons/02-dictionary-basico"; +import { leccion03 } from "./lessons/03-buscar-seguro"; +import { leccion04 } from "./lessons/04-actualizar-y-eliminar"; +import { leccion05 } from "./lessons/05-elegir-coleccion"; + +import { practice } from "./practice"; + +export const csharpPoo2Diccionarios = defineUnit({ + slug: "csharp-poo2-02-diccionarios", + title: "Diccionarios de objetos", + description: + "Organiza objetos por una clave estable y realiza altas, consultas, actualizaciones y bajas sin recorrer toda la colección.", + icon: "🗂️", + published: false, + lessons: [leccion01, leccion02, leccion03, leccion04, leccion05], + practice, +}); diff --git a/prisma/content/courses/csharp-poo-1/units/10-diccionarios/lessons/01-clave-y-valor.ts b/prisma/content/courses/csharp-poo-1/units/10-diccionarios/lessons/01-clave-y-valor.ts new file mode 100644 index 0000000..861693c --- /dev/null +++ b/prisma/content/courses/csharp-poo-1/units/10-diccionarios/lessons/01-clave-y-valor.ts @@ -0,0 +1,129 @@ +import { defineLesson } from "../../../../../authoring"; + +export const leccion01 = defineLesson({ + slug: "clave-y-valor", + title: "Clave y valor: buscar por identidad", + description: + "Distingue una posición accidental de una clave del dominio y decide cuándo un diccionario representa mejor el acceso.", + estimatedMinutes: 13, + xpReward: 45, + steps: [ + { + type: "theory", + markdown: `# Una clave responde “¿quién es?”, no “¿dónde está?” + +En una lista, encontrar el producto \`P-104\` normalmente exige recorrer elementos hasta comparar su código. Esa búsqueda puede ser correcta, pero el código del producto ya es una **clave**: representa identidad dentro del inventario. + +Un diccionario organiza pares **clave → valor**. En C# se expresa como \`Dictionary\`: + +- \`TKey\` es el tipo de la clave; +- \`TValue\` es el tipo del objeto o dato asociado. + +Ejemplo: \`Dictionary\` permite preguntar por el código del producto sin depender de su posición. + +Una buena clave debe ser estable dentro del problema. El nombre visible de una persona puede repetirse; un número de registro suele ser mejor clave.`, + }, + { + type: "code_example", + code: `using System; +using System.Collections.Generic; + +class Producto +{ + public string Nombre { get; private set; } + public Producto(string nombre) { Nombre = nombre; } +} + +class Program +{ + static void Main() + { + Dictionary porCodigo = new Dictionary(); + porCodigo.Add("A1", new Producto("Mouse")); + porCodigo.Add("B2", new Producto("Teclado")); + + Console.WriteLine(porCodigo["B2"].Nombre); + } +}`, + explanation: + "la clave `B2` localiza el objeto. El índice del diccionario no es un número de posición: es una clave del dominio.", + runnable: true, + expectedOutput: `Teclado`, + }, + { + type: "matching", + pairs: [ + { left: "Código de producto único", right: "Buena clave de diccionario." }, + { left: "Posición 4 de una lista", right: "Ubicación accidental, no identidad." }, + { + left: "Dictionary", + right: "Código de texto asociado a un objeto.", + }, + { + left: "Dos alumnos con el mismo nombre", + right: "Señal de que el nombre no es una clave suficiente.", + }, + ], + }, + { + type: "code_challenge", + exercise: { + prompt: + "Lee `n` pares `codigo` y `precio` (cada dato en una línea) y guárdalos en un `Dictionary`. Después lee un código que siempre existe e imprime `Precio: X` usando la clave.", + starterCode: `using System; +using System.Collections.Generic; +class Program +{ + static void Main() + { + int n = int.Parse(Console.ReadLine()); + // diccionario y carga + // consulta final + } +}`, + solutionCode: `using System; +using System.Collections.Generic; +class Program +{ + static void Main() + { + int n = int.Parse(Console.ReadLine()); + Dictionary precios = new Dictionary(); + for (int i = 0; i < n; i++) + { + string codigo = Console.ReadLine(); + int precio = int.Parse(Console.ReadLine()); + precios.Add(codigo, precio); + } + string buscado = Console.ReadLine(); + Console.WriteLine("Precio: " + precios[buscado]); + } +}`, + hints: [ + "el primer tipo genérico es la clave", + "`Add(clave, valor)` registra el par", + "consulta con `diccionario[clave]`", + ], + difficulty: "easy", + xpReward: 24, + testCases: [ + { + visible: true, + stdin: "2\nA1\n120\nB2\n80\nB2\n", + expectedStdout: "Precio: 80\n", + }, + { + visible: false, + stdin: "1\nX\n0\nX\n", + expectedStdout: "Precio: 0\n", + }, + { + visible: false, + stdin: "3\nP1\n10\nP2\n20\nP3\n30\nP1\n", + expectedStdout: "Precio: 10\n", + }, + ], + }, + }, + ], +}); diff --git a/prisma/content/courses/csharp-poo-1/units/10-diccionarios/lessons/02-dictionary-basico.ts b/prisma/content/courses/csharp-poo-1/units/10-diccionarios/lessons/02-dictionary-basico.ts new file mode 100644 index 0000000..2a98b7a --- /dev/null +++ b/prisma/content/courses/csharp-poo-1/units/10-diccionarios/lessons/02-dictionary-basico.ts @@ -0,0 +1,138 @@ +import { defineLesson } from "../../../../../authoring"; + +export const leccion02 = defineLesson({ + slug: "dictionary-basico", + title: "Dictionary básico con objetos", + description: + "Da de alta objetos con claves únicas y diferencia `Add` del indexador de asignación.", + estimatedMinutes: 15, + xpReward: 50, + steps: [ + { + type: "theory", + markdown: `# Una clave sólo puede tener un valor a la vez + +\`Dictionary\` exige claves únicas. Si haces \`Add("A1", producto)\` dos veces, el segundo \`Add\` falla: el diccionario no puede adivinar si querías reemplazar, duplicar o corregir. + +Existen dos operaciones parecidas pero con intención distinta: + +- \`Add(clave, valor)\`: alta nueva; exige que la clave no exista. +- \`diccionario[clave] = valor\`: asigna; crea si no existe o reemplaza si ya existe. + +Para una operación de “alta” conviene detectar duplicados explícitamente. Para una actualización conocida, el indexador puede ser apropiado.`, + }, + { + type: "code_example", + code: `using System; +using System.Collections.Generic; + +class Producto +{ + public string Codigo { get; private set; } + public int Stock { get; private set; } + public Producto(string codigo, int stock) + { + Codigo = codigo; + Stock = stock; + } +} + +class Program +{ + static void Main() + { + Dictionary inventario = new Dictionary(); + Producto p = new Producto("P1", 5); + inventario.Add(p.Codigo, p); + + Producto recuperado = inventario["P1"]; + Console.WriteLine(recuperado.Codigo + ": " + recuperado.Stock); + } +}`, + explanation: + "la clave se toma del mismo objeto para no crear dos fuentes de identidad diferentes.", + runnable: true, + expectedOutput: `P1: 5`, + }, + { + type: "quiz", + question: + "¿qué expresa mejor una operación que debe rechazar productos duplicados?", + options: [ + "Sobrescribir siempre con `inventario[codigo] = producto`.", + "Comprobar si la clave existe y rechazar antes de `Add`.", + "Cambiar el código del producto hasta encontrar uno libre.", + "Guardar el producto dos veces con la misma clave.", + ], + correctIndex: 1, + explanation: + "la regla de negocio debe ser explícita; sobrescribir silenciosamente oculta un alta duplicada.", + }, + { + type: "code_challenge", + exercise: { + prompt: + "Crea `Producto` (`Codigo`, `Stock`). Lee `n` altas. Si un código aparece por primera vez, guarda el producto e imprime `ALTA`. Si el código ya existe, no reemplaces el objeto e imprime `DUPLICADO`. Al final imprime `Total: X`.", + starterCode: `using System; +using System.Collections.Generic; +class Producto { } +class Program { static void Main() { } }`, + solutionCode: `using System; +using System.Collections.Generic; +class Producto +{ + public string Codigo { get; private set; } + public int Stock { get; private set; } + public Producto(string codigo, int stock) { Codigo = codigo; Stock = stock; } +} +class Program +{ + static void Main() + { + int n = int.Parse(Console.ReadLine()); + Dictionary inventario = new Dictionary(); + for (int i = 0; i < n; i++) + { + string codigo = Console.ReadLine(); + int stock = int.Parse(Console.ReadLine()); + if (inventario.ContainsKey(codigo)) + { + Console.WriteLine("DUPLICADO"); + } + else + { + inventario.Add(codigo, new Producto(codigo, stock)); + Console.WriteLine("ALTA"); + } + } + Console.WriteLine("Total: " + inventario.Count); + } +}`, + hints: [ + "`ContainsKey` pregunta por identidad", + "no uses el indexador antes de saber si existe", + "`Count` cuenta claves vigentes", + ], + difficulty: "medium", + xpReward: 30, + testCases: [ + { + visible: true, + stdin: "3\nA\n1\nB\n2\nA\n9\n", + expectedStdout: "ALTA\nALTA\nDUPLICADO\nTotal: 2\n", + }, + { + visible: false, + stdin: "1\nX\n0\n", + expectedStdout: "ALTA\nTotal: 1\n", + }, + { + visible: false, + stdin: "4\nP\n1\nP\n2\nP\n3\nQ\n4\n", + expectedStdout: "ALTA\nDUPLICADO\nDUPLICADO\nALTA\nTotal: 2\n", + }, + ], + }, + }, + ], +}); diff --git a/prisma/content/courses/csharp-poo-1/units/10-diccionarios/lessons/03-buscar-seguro.ts b/prisma/content/courses/csharp-poo-1/units/10-diccionarios/lessons/03-buscar-seguro.ts new file mode 100644 index 0000000..2f1e8e6 --- /dev/null +++ b/prisma/content/courses/csharp-poo-1/units/10-diccionarios/lessons/03-buscar-seguro.ts @@ -0,0 +1,121 @@ +import { defineLesson } from "../../../../../authoring"; + +export const leccion03 = defineLesson({ + slug: "buscar-seguro", + title: "Búsqueda segura con TryGetValue", + description: + "Consulta claves que pueden no existir sin convertir la ausencia en una excepción accidental.", + estimatedMinutes: 14, + xpReward: 50, + steps: [ + { + type: "theory", + markdown: `# “No existe” también es un resultado normal + +El indexador \`diccionario[clave]\` asume que la clave existe. Si no existe, lanza una excepción. Eso es útil cuando la ausencia indica un error de programación, pero una búsqueda de usuario normalmente puede fallar sin que el programa esté roto. + +Dos herramientas comunes: + +- \`ContainsKey(clave)\`: pregunta primero y luego accedes. +- \`TryGetValue(clave, out valor)\`: pregunta y recupera en una sola operación. + +\`TryGetValue\` devuelve \`true\` si encontró la clave y deja el objeto en la variable \`out\`. Si devuelve \`false\`, puedes responder “no encontrado” de manera explícita.`, + }, + { + type: "code_example", + code: `using System; +using System.Collections.Generic; +class Program +{ + static void Main() + { + Dictionary stock = new Dictionary(); + stock.Add("A1", 4); + + int cantidad; + if (stock.TryGetValue("B2", out cantidad)) + Console.WriteLine(cantidad); + else + Console.WriteLine("No encontrado"); + } +}`, + explanation: + "la ausencia de `B2` sigue el flujo normal del `if`; no depende de capturar una excepción.", + runnable: true, + expectedOutput: `No encontrado`, + }, + { + type: "fill_blank", + prompt: + "completa una consulta segura que recupera el stock sólo si existe la clave.", + template: `int stock; +if (inventario.{{0}}(codigo, {{1}} stock)) +{ + Console.WriteLine(stock); +} +else +{ + Console.WriteLine("NO"); +}`, + blanks: [{ answer: "TryGetValue" }, { answer: "out" }], + }, + { + type: "code_challenge", + exercise: { + prompt: + "Lee `n` pares `codigo`/`stock`, crea un diccionario y después lee `q` consultas. Para cada código consultado imprime su stock o `NO` si la clave no existe. Usa `TryGetValue`.", + starterCode: `using System; +using System.Collections.Generic; +class Program { static void Main() { } }`, + solutionCode: `using System; +using System.Collections.Generic; +class Program +{ + static void Main() + { + int n = int.Parse(Console.ReadLine()); + Dictionary stock = new Dictionary(); + for (int i = 0; i < n; i++) + { + string codigo = Console.ReadLine(); + stock[codigo] = int.Parse(Console.ReadLine()); + } + + int q = int.Parse(Console.ReadLine()); + for (int i = 0; i < q; i++) + { + string codigo = Console.ReadLine(); + int cantidad; + if (stock.TryGetValue(codigo, out cantidad)) Console.WriteLine(cantidad); + else Console.WriteLine("NO"); + } + } +}`, + hints: [ + "declara la variable antes de `TryGetValue`", + "el segundo argumento lleva `out`", + "cada consulta produce exactamente una línea", + ], + difficulty: "easy", + xpReward: 26, + testCases: [ + { + visible: true, + stdin: "2\nA\n5\nB\n2\n3\nA\nX\nB\n", + expectedStdout: "5\nNO\n2\n", + }, + { + visible: false, + stdin: "1\nZ\n0\n2\nZ\nY\n", + expectedStdout: "0\nNO\n", + }, + { + visible: false, + stdin: "3\nA\n1\nB\n2\nC\n3\n1\nC\n", + expectedStdout: "3\n", + }, + ], + }, + }, + ], +}); diff --git a/prisma/content/courses/csharp-poo-1/units/10-diccionarios/lessons/04-actualizar-y-eliminar.ts b/prisma/content/courses/csharp-poo-1/units/10-diccionarios/lessons/04-actualizar-y-eliminar.ts new file mode 100644 index 0000000..d30c267 --- /dev/null +++ b/prisma/content/courses/csharp-poo-1/units/10-diccionarios/lessons/04-actualizar-y-eliminar.ts @@ -0,0 +1,134 @@ +import { defineLesson } from "../../../../../authoring"; + +export const leccion04 = defineLesson({ + slug: "actualizar-y-eliminar", + title: "Actualizar y eliminar por clave", + description: + "Realiza cambios dirigidos a una identidad estable y diferencia actualizar el objeto de reemplazar la asociación.", + estimatedMinutes: 15, + xpReward: 55, + steps: [ + { + type: "theory", + markdown: `# CRUD también existe en memoria + +Un diccionario permite expresar operaciones de mantenimiento con una clave: + +- **Create:** \`Add\`; +- **Read:** indexador o \`TryGetValue\`; +- **Update:** modificar el objeto recuperado o reemplazar el valor asociado; +- **Delete:** \`Remove(clave)\`. + +Si el objeto encapsula su estado, es preferible pedirle una operación (\`AjustarStock\`) en lugar de exponer un setter público. El diccionario localiza; el objeto protege sus reglas.`, + }, + { + type: "code_example", + code: `using System; +using System.Collections.Generic; +class Producto +{ + public int Stock { get; private set; } + public Producto(int stock) { Stock = stock; } + public void AjustarStock(int cambio) { Stock += cambio; } +} +class Program +{ + static void Main() + { + Dictionary inventario = new Dictionary(); + inventario.Add("P1", new Producto(5)); + inventario["P1"].AjustarStock(3); + Console.WriteLine(inventario["P1"].Stock); + inventario.Remove("P1"); + Console.WriteLine(inventario.Count); + } +}`, + explanation: + "la clave encuentra el producto; la propia clase ejecuta la modificación de stock; `Remove` retira la asociación completa.", + runnable: true, + expectedOutput: `8\n0`, + }, + { + type: "quiz", + question: + "tienes `Dictionary` y `Producto.Stock` tiene `private set`. ¿Qué diseño conserva mejor el encapsulamiento?", + options: [ + "Hacer público el setter para que el diccionario lo cambie.", + "Añadir un método de dominio en `Producto`, recuperar el objeto y llamar ese método.", + "Reemplazar el diccionario por variables globales.", + "Convertir `Stock` en `static`.", + ], + correctIndex: 1, + explanation: + "Añadir un método de dominio en `Producto`, recuperar el objeto y llamar ese método: el diccionario sólo localiza, la clase sigue protegiendo su propio estado.", + }, + { + type: "code_challenge", + exercise: { + prompt: + "Lee un inventario inicial y luego `q` comandos:\n\n- `U CODIGO CAMBIO`: suma `CAMBIO` al stock si existe; si no, no hace nada.\n- `E CODIGO`: elimina la clave si existe.\n- `B CODIGO`: imprime stock o `NO`.\n\nUsa un `Dictionary`; los códigos no tienen espacios.", + starterCode: `using System; +using System.Collections.Generic; +class Program { static void Main() { } }`, + solutionCode: `using System; +using System.Collections.Generic; +class Program +{ + static void Main() + { + int n = int.Parse(Console.ReadLine()); + Dictionary inventario = new Dictionary(); + for (int i = 0; i < n; i++) + { + string codigo = Console.ReadLine(); + inventario[codigo] = int.Parse(Console.ReadLine()); + } + + int q = int.Parse(Console.ReadLine()); + for (int i = 0; i < q; i++) + { + string[] p = Console.ReadLine().Split(' '); + if (p[0] == "U") + { + if (inventario.ContainsKey(p[1])) inventario[p[1]] += int.Parse(p[2]); + } + else if (p[0] == "E") + { + inventario.Remove(p[1]); + } + else if (p[0] == "B") + { + int valor; + Console.WriteLine(inventario.TryGetValue(p[1], out valor) ? valor.ToString() : "NO"); + } + } + } +}`, + hints: [ + "`Remove` ya tolera una clave ausente", + "actualiza sólo si `ContainsKey`", + "una búsqueda ausente no es excepción", + ], + difficulty: "medium", + xpReward: 34, + testCases: [ + { + visible: true, + stdin: "2\nA\n5\nB\n2\n5\nB A\nU A 3\nB A\nE A\nB A\n", + expectedStdout: "5\n8\nNO\n", + }, + { + visible: false, + stdin: "1\nX\n0\n3\nU Y 5\nB X\nB Y\n", + expectedStdout: "0\nNO\n", + }, + { + visible: false, + stdin: "2\nA\n1\nB\n9\n4\nE B\nB B\nU A -1\nB A\n", + expectedStdout: "NO\n0\n", + }, + ], + }, + }, + ], +}); diff --git a/prisma/content/courses/csharp-poo-1/units/10-diccionarios/lessons/05-elegir-coleccion.ts b/prisma/content/courses/csharp-poo-1/units/10-diccionarios/lessons/05-elegir-coleccion.ts new file mode 100644 index 0000000..7f3cb2b --- /dev/null +++ b/prisma/content/courses/csharp-poo-1/units/10-diccionarios/lessons/05-elegir-coleccion.ts @@ -0,0 +1,123 @@ +import { defineLesson } from "../../../../../authoring"; + +export const leccion05 = defineLesson({ + slug: "elegir-coleccion", + title: "Elegir la colección correcta", + description: + "Selecciona lista, pila, cola o diccionario según las operaciones dominantes en lugar de usar siempre la misma estructura.", + estimatedMinutes: 16, + xpReward: 55, + steps: [ + { + type: "theory", + markdown: `# La estructura expresa qué operaciones importan + +No existe una colección “mejor” en abstracto. + +- \`List\`: secuencia general; recorres, accedes por índice y mantienes orden de inserción. +- \`Stack\`: el elemento más reciente debe salir primero. +- \`Queue\`: el elemento más antiguo debe salir primero. +- \`Dictionary\`: la operación dominante es localizar por una clave estable. + +Antes de escribir código pregunta: **¿cómo se identifica un elemento y en qué orden debe salir?** Esa respuesta suele indicar la estructura. + +También puedes combinar estructuras. Un sistema de soporte puede usar un diccionario para localizar tickets por folio y una cola para decidir cuál atender después.`, + }, + { + type: "matching", + pairs: [ + { left: "Historial de deshacer", right: "`Stack`" }, + { left: "Turnos de ventanilla", right: "`Queue`" }, + { left: "Productos localizados por SKU", right: "`Dictionary`" }, + { left: "Secuencia editable de materias", right: "`List`" }, + ], + }, + { + type: "code_example", + code: `using System; +using System.Collections.Generic; +class Ticket +{ + public int Folio { get; private set; } + public string Asunto { get; private set; } + public Ticket(int folio, string asunto) { Folio = folio; Asunto = asunto; } +} +class Program +{ + static void Main() + { + Dictionary porFolio = new Dictionary(); + Queue pendientes = new Queue(); + + Ticket t = new Ticket(101, "Sin red"); + porFolio.Add(t.Folio, t); + pendientes.Enqueue(t.Folio); + + int siguiente = pendientes.Dequeue(); + Console.WriteLine(porFolio[siguiente].Asunto); + } +}`, + explanation: + "el diccionario resuelve “qué ticket es” y la cola resuelve “cuál sigue”. Dos preguntas distintas justifican dos estructuras.", + runnable: true, + expectedOutput: `Sin red`, + }, + { + type: "code_challenge", + exercise: { + prompt: + "Modela pedidos con dos estructuras:\n\n- `Dictionary` guarda `folio → piezas`;\n- `Queue` guarda el orden de atención.\n\nLee `n` pedidos (`folio` y `piezas`, una línea por dato). Después atiéndelos todos en orden FIFO e imprime `FOLIO: PIEZAS`.", + starterCode: `using System; +using System.Collections.Generic; +class Program { static void Main() { } }`, + solutionCode: `using System; +using System.Collections.Generic; +class Program +{ + static void Main() + { + int n = int.Parse(Console.ReadLine()); + Dictionary porFolio = new Dictionary(); + Queue orden = new Queue(); + for (int i = 0; i < n; i++) + { + string folio = Console.ReadLine(); + int piezas = int.Parse(Console.ReadLine()); + porFolio.Add(folio, piezas); + orden.Enqueue(folio); + } + while (orden.Count > 0) + { + string folio = orden.Dequeue(); + Console.WriteLine(folio + ": " + porFolio[folio]); + } + } +}`, + hints: [ + "guarda la misma clave en la cola", + "la cola determina orden, el diccionario recupera datos", + "no recorras el diccionario para decidir qué sigue", + ], + difficulty: "medium", + xpReward: 32, + testCases: [ + { + visible: true, + stdin: "2\nF1\n3\nF2\n7\n", + expectedStdout: "F1: 3\nF2: 7\n", + }, + { + visible: false, + stdin: "1\nX9\n0\n", + expectedStdout: "X9: 0\n", + }, + { + visible: false, + stdin: "3\nA\n2\nC\n1\nB\n9\n", + expectedStdout: "A: 2\nC: 1\nB: 9\n", + }, + ], + }, + }, + ], +}); diff --git a/prisma/content/courses/csharp-poo-1/units/10-diccionarios/practice.ts b/prisma/content/courses/csharp-poo-1/units/10-diccionarios/practice.ts new file mode 100644 index 0000000..b6a343a --- /dev/null +++ b/prisma/content/courses/csharp-poo-1/units/10-diccionarios/practice.ts @@ -0,0 +1,195 @@ +import type { PracticeExerciseDefinition } from "../../../../exercises/types"; + +export const practice = [ + { + slug: "csharp-poo2-indice-productos", + title: "Índice de productos", + description: + "Construye un índice por código y consulta varios productos sin recorrer una lista.", + prompt: + "lee `n` pares `codigo`/`nombre`, después `q` códigos. Imprime el nombre o `NO` por cada consulta usando `Dictionary`.", + starterCode: + "using System;\nusing System.Collections.Generic;\nclass Program { static void Main() { } }", + solutionCode: `using System; +using System.Collections.Generic; +class Program +{ + static void Main() + { + int n=int.Parse(Console.ReadLine()); + Dictionary d=new Dictionary(); + for(int i=0;i cal=new Dictionary(); + for(int i=0;i d=new Dictionary(); + for(int i=0;i` privado, `Agregar`, `Buscar` y `Eliminar`. Procesa comandos `A id nombre`, `B id`, `E id`. Los nombres del ejercicio son una sola palabra. `A` imprime `OK` o `DUP`; `B` imprime nombre o `NO`; `E` no imprime.", + starterCode: + "using System;\nusing System.Collections.Generic;\nclass Cliente { }\nclass Directorio { }\nclass Program { static void Main() { } }", + solutionCode: `using System; +using System.Collections.Generic; +class Cliente +{ + public string Id{get;private set;} public string Nombre{get;private set;} + public Cliente(string id,string nombre){Id=id;Nombre=nombre;} +} +class Directorio +{ + private Dictionary clientes=new Dictionary(); + public bool Agregar(Cliente c){if(clientes.ContainsKey(c.Id))return false;clientes.Add(c.Id,c);return true;} + public Cliente Buscar(string id){Cliente c;return clientes.TryGetValue(id,out c)?c:null;} + public void Eliminar(string id){clientes.Remove(id);} +} +class Program +{ + static void Main() + { + int n=int.Parse(Console.ReadLine()); Directorio d=new Directorio(); + for(int i=0;i b.Precio) return 1; + return 0; + } + + static void Main() + { + Producto a = new Producto("Mouse", 250); + Producto b = new Producto("Teclado", 400); + Console.WriteLine(CompararPorPrecio(a, b)); + } +}`, + explanation: + "el método no ordena nada todavía; define la regla que un algoritmo puede usar después.", + runnable: true, + expectedOutput: "-1", + }, + { + type: "matching", + pairs: [ + { + left: "Ordenar catálogo por precio ascendente", + right: "Comparar Precio menor primero", + }, + { + left: "Ranking por puntuación", + right: "Comparar Puntos mayor primero", + }, + { + left: "Directorio alfabético", + right: "Comparar Nombre", + }, + { + left: "Misma regla, dos objetos equivalentes", + right: "Comparador devuelve 0", + }, + ], + }, + { + type: "code_challenge", + exercise: { + prompt: + "Crea `Producto` con `Nombre` y `Precio`. Implementa `CompararPorPrecio(Producto a, Producto b)` que devuelva `-1`, `0` o `1`. Lee dos productos (nombre y precio de cada uno) e imprime el nombre del que debe aparecer primero; si tienen el mismo precio imprime `EMPATE`.", + starterCode: `using System; +class Producto { } +class Program +{ + static int CompararPorPrecio(Producto a, Producto b) { return 0; } + static void Main() { } +}`, + solutionCode: `using System; +class Producto +{ + public string Nombre { get; private set; } + public int Precio { get; private set; } + public Producto(string nombre, int precio) { Nombre=nombre; Precio=precio; } +} +class Program +{ + static int CompararPorPrecio(Producto a, Producto b) + { + if(a.Preciob.Precio)return 1; + return 0; + } + static void Main() + { + Producto a=new Producto(Console.ReadLine(),int.Parse(Console.ReadLine())); + Producto b=new Producto(Console.ReadLine(),int.Parse(Console.ReadLine())); + int c=CompararPorPrecio(a,b); + if(c<0)Console.WriteLine(a.Nombre); + else if(c>0)Console.WriteLine(b.Nombre); + else Console.WriteLine("EMPATE"); + } +}`, + hints: [ + "compara precios, no nombres", + "devuelve cero si son iguales", + "el signo decide quién va primero", + ], + difficulty: "easy", + xpReward: 24, + testCases: [ + { + stdin: "Mouse\n250\nTeclado\n400\n", + expectedStdout: "Mouse\n", + visible: true, + }, + { + stdin: "A\n9\nB\n2\n", + expectedStdout: "B\n", + visible: false, + }, + { + stdin: "A\n5\nB\n5\n", + expectedStdout: "EMPATE\n", + visible: false, + }, + ], + }, + }, + ], +}); diff --git a/prisma/content/courses/csharp-poo-1/units/11-ordenamiento/lessons/02-burbuja-con-objetos.ts b/prisma/content/courses/csharp-poo-1/units/11-ordenamiento/lessons/02-burbuja-con-objetos.ts new file mode 100644 index 0000000..ac9e54e --- /dev/null +++ b/prisma/content/courses/csharp-poo-1/units/11-ordenamiento/lessons/02-burbuja-con-objetos.ts @@ -0,0 +1,128 @@ +import { defineLesson } from "../../../../../authoring"; + +export const leccion02 = defineLesson({ + slug: "burbuja-con-objetos", + title: "Burbuja con objetos", + description: + "Implementa un ordenamiento básico sobre una lista de objetos y razona cada intercambio.", + estimatedMinutes: 17, + xpReward: 60, + steps: [ + { + type: "theory", + markdown: `# Burbuja: comparar vecinos y corregir inversiones + +El ordenamiento burbuja recorre pares vecinos. Si están al revés según el criterio, los intercambia. Después de una pasada completa, uno de los elementos extremos queda en su lugar definitivo. + +Para \`n\` elementos, una implementación pedagógica usa dos ciclos: + +- el externo controla cuántas pasadas faltan; +- el interno compara \`j\` con \`j + 1\`. + +No es el algoritmo más eficiente para colecciones grandes. Aquí importa porque obliga a hacer explícitos el criterio, la comparación y el intercambio. Más adelante puedes usar implementaciones de biblioteca entendiendo qué problema resuelven.`, + }, + { + type: "code_example", + code: `using System; +using System.Collections.Generic; +class Producto +{ + public string Nombre { get; private set; } + public int Precio { get; private set; } + public Producto(string nombre,int precio){Nombre=nombre;Precio=precio;} +} +class Program +{ + static void Main() + { + List p=new List(); + p.Add(new Producto("A",30)); + p.Add(new Producto("B",10)); + p.Add(new Producto("C",20)); + + for(int pasada=0;pasadap[j+1].Precio) + { + Producto temp=p[j]; p[j]=p[j+1]; p[j+1]=temp; + } + } + } + + for(int i=0;i p=new List(); + for(int i=0;ip[j+1].Precio){Producto t=p[j];p[j]=p[j+1];p[j+1]=t;} + for(int i=0;i p=new List(); + p.Add(new Producto("Teclado")); p.Add(new Producto("Cable")); p.Add(new Producto("Mouse")); + + for(int i=1;i=0 && string.Compare(p[j].Nombre,actual.Nombre,StringComparison.Ordinal)>0) + { + p[j+1]=p[j]; + j--; + } + p[j+1]=actual; + } + + for(int i=0;i nombres=new List(); + for(int i=0;i=0 && string.Compare(nombres[j],actual,StringComparison.Ordinal)>0) + { nombres[j+1]=nombres[j]; j--; } + nombres[j+1]=actual; + } + for(int i=0;i", + description: + "Separa el criterio de orden de la entidad y permite ordenar el mismo tipo de varias maneras.", + estimatedMinutes: 16, + xpReward: 55, + steps: [ + { + type: "theory", + markdown: `# Un producto puede tener más de un orden válido + +Si \`Producto\` se ordena a veces por precio y a veces por stock, meter una única regla dentro de la clase vuelve rígido el diseño. + +\`IComparer\` representa un **objeto comparador**. Su método \`Compare(a,b)\` encapsula una regla concreta. Puedes crear \`ComparadorPorPrecio\`, \`ComparadorPorStock\` y reutilizarlos sin modificar \`Producto\`. + +\`List.Sort(comparador)\` aprovecha un algoritmo de biblioteca; aquí ya entendiste algoritmos básicos manuales y ahora separas dos responsabilidades: + +- la biblioteca ejecuta el ordenamiento; +- tu comparador define el criterio.`, + }, + { + type: "code_example", + code: `using System; +using System.Collections.Generic; +class Producto +{ + public string Nombre{get;private set;} public int Stock{get;private set;} + public Producto(string nombre,int stock){Nombre=nombre;Stock=stock;} +} +class ComparadorPorStock : IComparer +{ + public int Compare(Producto a, Producto b) + { + if(a.Stockb.Stock)return 1; + return string.Compare(a.Nombre,b.Nombre,StringComparison.Ordinal); + } +} +class Program +{ + static void Main() + { + List p=new List(); + p.Add(new Producto("B",5)); p.Add(new Producto("A",2)); p.Add(new Producto("C",5)); + p.Sort(new ComparadorPorStock()); + for(int i=0;i +{ + public int {{1}}(Producto a, Producto b) + { + return a.Stock.CompareTo(b.Stock); + } +} + +productos.{{2}}(new Comparador());`, + blanks: [ + { answer: "IComparer" }, + { answer: "Compare" }, + { answer: "Sort" }, + ], + }, + { + type: "code_challenge", + exercise: { + prompt: + "Crea `Alumno(Nombre,Promedio)` y `ComparadorPromedio : IComparer` que ordene **promedio descendente**. Si dos promedios son iguales, desempata por nombre ascendente con `string.Compare(..., StringComparison.Ordinal)`. Lee `n`, ordena con `List.Sort(comparador)` e imprime `Nombre Promedio`.", + starterCode: `using System; +using System.Collections.Generic; +class Alumno { } +class ComparadorPromedio : IComparer { } +class Program { static void Main() { } }`, + solutionCode: `using System; +using System.Collections.Generic; +class Alumno +{ + public string Nombre{get;private set;} public int Promedio{get;private set;} + public Alumno(string nombre,int promedio){Nombre=nombre;Promedio=promedio;} +} +class ComparadorPromedio : IComparer +{ + public int Compare(Alumno a,Alumno b) + { + if(a.Promedio>b.Promedio)return -1; + if(a.Promedio alumnos=new List(); + for(int i=0;i a=new List();for(int i=0;ia[j+1].Precio){Producto t=a[j];a[j]=a[j+1];a[j+1]=t;}for(int i=0;i nombres=new List(); + for(int i=0;i=0 && string.Compare(nombres[j],actual,StringComparison.Ordinal)>0) + { nombres[j+1]=nombres[j]; j--; } + nombres[j+1]=actual; + } + for(int i=0;i`.", + prompt: + "crea `Producto(Codigo,Stock)` y un `IComparer` que ordene stock descendente y código ascendente en empate. Usa `List.Sort(comparador)` e imprime `codigo stock`.", + starterCode: `using System; +using System.Collections.Generic; +class Producto { } +class Comparador : IComparer { } +class Program { static void Main() { } }`, + solutionCode: `using System;using System.Collections.Generic; +class Producto{public string Codigo{get;private set;}public int Stock{get;private set;}public Producto(string c,int s){Codigo=c;Stock=s;}} +class Comparador:IComparer{public int Compare(Producto a,Producto b){if(a.Stock>b.Stock)return -1;if(a.Stock a=new List();for(int i=0;i=0?"ENCONTRADO "+pos:"NO");}}`, + difficulty: "medium", + xpReward: 32, + testCases: [ + { + stdin: "5\n10\n20\n30\n40\n50\n40\n", + expectedStdout: "ENCONTRADO 3\n", + visible: true, + }, + { + stdin: "4\n1\n2\n3\n4\n9\n", + expectedStdout: "NO\n", + visible: false, + }, + { + stdin: "1\n7\n7\n", + expectedStdout: "ENCONTRADO 0\n", + visible: false, + }, + ], + }, +] satisfies PracticeExerciseDefinition[]; diff --git a/prisma/content/courses/csharp-poo-1/units/12-xml/index.ts b/prisma/content/courses/csharp-poo-1/units/12-xml/index.ts new file mode 100644 index 0000000..c60ada1 --- /dev/null +++ b/prisma/content/courses/csharp-poo-1/units/12-xml/index.ts @@ -0,0 +1,20 @@ +import { defineUnit } from "../../../../authoring"; + +import { leccion01 } from "./lessons/01-que-es-xml"; +import { leccion02 } from "./lessons/02-bien-formado-y-valido"; +import { leccion03 } from "./lessons/03-objetos-a-xml"; +import { leccion04 } from "./lessons/04-xml-a-objetos"; +import { leccion05 } from "./lessons/05-persistencia-xml"; + +import { practice } from "./practice"; + +export const csharpPoo2Xml = defineUnit({ + slug: "csharp-poo2-04-xml", + title: "XML y persistencia", + description: + "Representa objetos como XML, valida su estructura, transforma texto en datos tipados y separa serialización de persistencia física.", + icon: "🧾", + published: false, + lessons: [leccion01, leccion02, leccion03, leccion04, leccion05], + practice, +}); diff --git a/prisma/content/courses/csharp-poo-1/units/12-xml/lessons/01-que-es-xml.ts b/prisma/content/courses/csharp-poo-1/units/12-xml/lessons/01-que-es-xml.ts new file mode 100644 index 0000000..da013c7 --- /dev/null +++ b/prisma/content/courses/csharp-poo-1/units/12-xml/lessons/01-que-es-xml.ts @@ -0,0 +1,125 @@ +import { defineLesson } from "../../../../../authoring"; + +export const leccion01 = defineLesson({ + slug: "que-es-xml", + title: "XML como representación estructurada", + description: + "Lee elementos, atributos y jerarquía como una representación explícita de información del dominio.", + estimatedMinutes: 14, + xpReward: 50, + steps: [ + { + type: "theory", + markdown: `# XML describe estructura, no sólo texto + +XML usa **elementos** y **atributos** para representar información con jerarquía. + +\`\`\`xml + + Mouse + 4 + +\`\`\` + +Aquí: + +- \`producto\` es el elemento raíz; +- \`codigo\` es un atributo del producto; +- \`nombre\` y \`stock\` son elementos hijos; +- el valor \`4\` sigue siendo texto en el documento y tendrá que convertirse a \`int\` al entrar al modelo C#. + +XML no sabe qué es un \`Producto\` de C#. El programa debe definir cómo traducir entre el documento y sus objetos.`, + }, + { + type: "code_example", + code: `using System; +using System.Xml; + +class Program +{ + static void Main() + { + string xml = "Mouse4"; + XmlDocument doc = new XmlDocument(); + doc.LoadXml(xml); + + XmlElement raiz = doc.DocumentElement; + Console.WriteLine(raiz.GetAttribute("codigo")); + Console.WriteLine(raiz["nombre"].InnerText); + Console.WriteLine(raiz["stock"].InnerText); + } +}`, + explanation: + "`XmlDocument` construye un árbol en memoria. `DocumentElement` es la raíz y los hijos se consultan por etiqueta.", + runnable: true, + expectedOutput: `P1 +Mouse +4`, + }, + { + type: "matching", + pairs: [ + { left: "", right: "Elemento raíz." }, + { left: 'codigo="P1"', right: "Atributo." }, + { left: "4", right: "Elemento hijo con contenido textual." }, + { left: "XmlDocument", right: "Árbol XML cargado en memoria." }, + ], + }, + { + type: "code_challenge", + exercise: { + prompt: + 'Lee una sola línea que contiene XML con esta forma:\n\n```xml\nMouse4\n```\n\nCarga la línea con `XmlDocument` e imprime exactamente:\n\n```text\nCodigo: P1\nNombre: Mouse\nStock: 4\n```', + starterCode: `using System; +using System.Xml; +class Program +{ + static void Main() + { + string xml = Console.ReadLine(); + // carga y lee + } +}`, + solutionCode: `using System; +using System.Xml; +class Program +{ + static void Main() + { + string xml = Console.ReadLine(); + XmlDocument doc = new XmlDocument(); + doc.LoadXml(xml); + XmlElement p = doc.DocumentElement; + Console.WriteLine("Codigo: " + p.GetAttribute("codigo")); + Console.WriteLine("Nombre: " + p["nombre"].InnerText); + Console.WriteLine("Stock: " + p["stock"].InnerText); + } +}`, + hints: [ + "`LoadXml` recibe el texto.", + "`DocumentElement` es el producto.", + "`GetAttribute` y `InnerText` recuperan valores.", + ], + difficulty: "easy", + xpReward: 26, + testCases: [ + { + visible: true, + stdin: 'Mouse4\n', + expectedStdout: "Codigo: P1\nNombre: Mouse\nStock: 4\n", + }, + { + visible: false, + stdin: 'Cable HDMI0\n', + expectedStdout: "Codigo: X\nNombre: Cable HDMI\nStock: 0\n", + }, + { + visible: false, + stdin: 'Monitor17\n', + expectedStdout: "Codigo: Z9\nNombre: Monitor\nStock: 17\n", + }, + ], + }, + }, + ], +}); diff --git a/prisma/content/courses/csharp-poo-1/units/12-xml/lessons/02-bien-formado-y-valido.ts b/prisma/content/courses/csharp-poo-1/units/12-xml/lessons/02-bien-formado-y-valido.ts new file mode 100644 index 0000000..a0cbd34 --- /dev/null +++ b/prisma/content/courses/csharp-poo-1/units/12-xml/lessons/02-bien-formado-y-valido.ts @@ -0,0 +1,130 @@ +import { defineLesson } from "../../../../../authoring"; + +export const leccion02 = defineLesson({ + slug: "bien-formado-y-valido", + title: "Bien formado no significa válido para tu sistema", + description: + "Separa errores de sintaxis XML de reglas de estructura y negocio que el programa debe verificar.", + estimatedMinutes: 15, + xpReward: 50, + steps: [ + { + type: "theory", + markdown: `# Dos niveles de validez + +Un XML está **bien formado** cuando respeta las reglas sintácticas del lenguaje: etiquetas cerradas, un único elemento raíz, anidamiento correcto, atributos con comillas, etc. + +Esto puede estar bien formado: + +\`\`\`xml +azul +\`\`\` + +pero quizá no sea **válido para tu aplicación** si un producto exige \`codigo\`, \`nombre\` y \`stock\`. + +En XML formal, la validez también puede declararse mediante DTD o XSD. En cpp-ceti no abrimos ahora un curso de esquemas: la habilidad mínima es distinguir: + +1. sintaxis XML válida para el parser; +2. estructura esperada por el contrato de tu programa; +3. datos válidos para el dominio. + +\`XmlException\` detecta errores sintácticos. Tus propias comprobaciones detectan nodos faltantes o valores inválidos.`, + }, + { + type: "code_example", + code: `using System; +using System.Xml; +class Program +{ + static void Main() + { + string xml = "Mouse"; + try + { + XmlDocument doc = new XmlDocument(); + doc.LoadXml(xml); + Console.WriteLine("Bien formado"); + } + catch (XmlException) + { + Console.WriteLine("XML mal formado"); + } + } +}`, + explanation: + "el cierre de `nombre` falta; el parser rechaza la estructura antes de que el dominio intente leer productos.", + runnable: true, + expectedOutput: "XML mal formado", + }, + { + type: "quiz", + question: + '`-3` está bien formado. Si tu dominio prohíbe stock negativo, ¿qué conclusión es correcta?', + options: [ + "XML bien formado implica producto válido.", + "El parser XML debe conocer todas las reglas del negocio.", + "La sintaxis puede ser válida y los datos seguir violando el dominio.", + "El documento deja de ser XML.", + ], + correctIndex: 2, + explanation: + "La sintaxis puede ser válida y los datos seguir violando el dominio.", + }, + { + type: "code_challenge", + exercise: { + prompt: + "Lee una línea XML. Si `XmlDocument.LoadXml` falla, imprime `MAL FORMADO`. Si carga correctamente pero no existe el elemento `nombre`, imprime `FALTA NOMBRE`. En otro caso imprime `OK: valor`.", + starterCode: `using System; +using System.Xml; +class Program { static void Main() { } }`, + solutionCode: `using System; +using System.Xml; +class Program +{ + static void Main() + { + string xml = Console.ReadLine(); + try + { + XmlDocument doc = new XmlDocument(); + doc.LoadXml(xml); + XmlElement raiz = doc.DocumentElement; + XmlNode nombre = raiz["nombre"]; + if (nombre == null) Console.WriteLine("FALTA NOMBRE"); + else Console.WriteLine("OK: " + nombre.InnerText); + } + catch (XmlException) + { + Console.WriteLine("MAL FORMADO"); + } + } +}`, + hints: [ + "Separa `try/catch` de la validación del nodo.", + "Un nodo ausente produce `null`.", + "No captures cualquier excepción para ocultar errores de programación.", + ], + difficulty: "medium", + xpReward: 30, + testCases: [ + { + visible: true, + stdin: "Mouse\n", + expectedStdout: "OK: Mouse\n", + }, + { + visible: false, + stdin: "4\n", + expectedStdout: "FALTA NOMBRE\n", + }, + { + visible: false, + stdin: "X\n", + expectedStdout: "MAL FORMADO\n", + }, + ], + }, + }, + ], +}); diff --git a/prisma/content/courses/csharp-poo-1/units/12-xml/lessons/03-objetos-a-xml.ts b/prisma/content/courses/csharp-poo-1/units/12-xml/lessons/03-objetos-a-xml.ts new file mode 100644 index 0000000..ef237f4 --- /dev/null +++ b/prisma/content/courses/csharp-poo-1/units/12-xml/lessons/03-objetos-a-xml.ts @@ -0,0 +1,123 @@ +import { defineLesson } from "../../../../../authoring"; + +export const leccion03 = defineLesson({ + slug: "objetos-a-xml", + title: "De objetos a XML", + description: + "Construye un documento XML desde objetos sin mezclar el modelo de dominio con concatenaciones frágiles de texto.", + estimatedMinutes: 17, + xpReward: 60, + steps: [ + { + type: "theory", + markdown: `# Serializar es traducir una representación a otra + +Un objeto vive en memoria con tipos C#: \`int\`, \`string\`, referencias y métodos. XML es texto estructurado. **Serializar** significa traducir el estado que necesitas conservar o transferir a esa representación. + +Podrías concatenar strings, pero aparecen problemas de escape y estructura. Una API XML crea nodos y se encarga de representar caracteres especiales correctamente. + +Decide de forma deliberada qué va como atributo y qué como elemento. Una regla simple para este curso: + +- identificadores pequeños y propios del elemento pueden ir como atributos; +- datos del contenido pueden ir como hijos. + +No es una ley universal; es un contrato de representación que debe mantenerse estable entre productor y consumidor.`, + }, + { + type: "code_example", + code: `using System; +using System.Xml; +class Program +{ + static void Main() + { + XmlDocument doc = new XmlDocument(); + XmlElement producto = doc.CreateElement("producto"); + producto.SetAttribute("codigo", "P1"); + + XmlElement nombre = doc.CreateElement("nombre"); + nombre.InnerText = "Mouse & teclado"; + producto.AppendChild(nombre); + + doc.AppendChild(producto); + Console.WriteLine(doc.OuterXml); + } +}`, + explanation: + "`InnerText` escapa `&` como `&`. La API mantiene el documento bien formado.", + runnable: true, + expectedOutput: 'Mouse & teclado', + }, + { + type: "code_completion", + lines: [ + 'XmlElement producto = doc.CreateElement("producto");', + 'producto.SetAttribute("codigo", codigo);', + 'XmlElement nombre = doc.CreateElement("nombre");', + "nombre.InnerText = texto;", + "producto.AppendChild(nombre);", + "doc.AppendChild(producto);", + ], + }, + { + type: "code_challenge", + exercise: { + prompt: + 'Lee `codigo`, `nombre` y `stock`. Construye con `XmlDocument` exactamente esta forma, sin concatenar el XML manualmente:\n\n```xml\nMouse4\n```\n\nImprime `doc.OuterXml`.', + starterCode: `using System; +using System.Xml; +class Program { static void Main() { } }`, + solutionCode: `using System; +using System.Xml; +class Program +{ + static void Main() + { + string codigo = Console.ReadLine(); + string textoNombre = Console.ReadLine(); + int stock = int.Parse(Console.ReadLine()); + + XmlDocument doc = new XmlDocument(); + XmlElement producto = doc.CreateElement("producto"); + producto.SetAttribute("codigo", codigo); + + XmlElement nombre = doc.CreateElement("nombre"); + nombre.InnerText = textoNombre; + producto.AppendChild(nombre); + + XmlElement nodoStock = doc.CreateElement("stock"); + nodoStock.InnerText = stock.ToString(); + producto.AppendChild(nodoStock); + + doc.AppendChild(producto); + Console.WriteLine(doc.OuterXml); + } +}`, + hints: [ + "Crea el root primero.", + "`SetAttribute` para código.", + "Cada hijo se agrega con `AppendChild`.", + ], + difficulty: "medium", + xpReward: 34, + testCases: [ + { + visible: true, + stdin: "P1\nMouse\n4\n", + expectedStdout: 'Mouse4\n', + }, + { + visible: false, + stdin: "X\nCable HDMI\n0\n", + expectedStdout: 'Cable HDMI0\n', + }, + { + visible: false, + stdin: "A&B\nUno & Dos\n9\n", + expectedStdout: 'Uno & Dos9\n', + }, + ], + }, + }, + ], +}); diff --git a/prisma/content/courses/csharp-poo-1/units/12-xml/lessons/04-xml-a-objetos.ts b/prisma/content/courses/csharp-poo-1/units/12-xml/lessons/04-xml-a-objetos.ts new file mode 100644 index 0000000..7a24c70 --- /dev/null +++ b/prisma/content/courses/csharp-poo-1/units/12-xml/lessons/04-xml-a-objetos.ts @@ -0,0 +1,130 @@ +import { defineLesson } from "../../../../../authoring"; + +export const leccion04 = defineLesson({ + slug: "xml-a-objetos", + title: "De XML a objetos: parsing y casting", + description: + "Convierte texto XML a tipos del dominio y valida la frontera antes de construir objetos.", + estimatedMinutes: 17, + xpReward: 60, + steps: [ + { + type: "theory", + markdown: `# La frontera entra como texto + +Aunque XML contenga \`8\`, \`InnerText\` devuelve un \`string\`. El programa tiene que convertirlo a \`int\` y decidir qué hacer si no puede. + +Ese proceso tiene dos partes: + +- **parsing:** interpretar una representación textual (\`"8"\`) como un valor; +- **casting/conversión:** obtener el tipo que el modelo necesita. + +Evita construir un objeto inválido y "arreglarlo después". Primero valida la entrada; después llama al constructor con datos tipados. + +Una frontera robusta puede usar \`int.TryParse\`, comprobar nodos obligatorios y entonces crear \`Producto\`.`, + }, + { + type: "code_example", + code: `using System; +using System.Xml; +class Producto +{ + public string Codigo{get;private set;} public int Stock{get;private set;} + public Producto(string codigo,int stock){Codigo=codigo;Stock=stock;} +} +class Program +{ + static void Main() + { + XmlDocument doc=new XmlDocument(); + doc.LoadXml("8"); + XmlElement raiz=doc.DocumentElement; + int stock; + if(!int.TryParse(raiz["stock"].InnerText,out stock)) + { + Console.WriteLine("Stock invalido"); return; + } + Producto p=new Producto(raiz.GetAttribute("codigo"),stock); + Console.WriteLine(p.Codigo+": "+p.Stock); + } +}`, + explanation: + "el objeto sólo nace después de que el texto se convirtió correctamente a `int`.", + runnable: true, + expectedOutput: "P1: 8", + }, + { + type: "quiz", + question: + "¿por qué no basta con leer `InnerText` y asignarlo directamente a una propiedad `int`?", + options: [ + "Porque XML siempre cifra los números.", + "Porque `InnerText` es texto y el modelo exige una conversión validada.", + "Porque C# no admite enteros en objetos.", + "Porque un atributo no puede contener números.", + ], + correctIndex: 1, + explanation: + "Porque `InnerText` es texto y el modelo exige una conversión validada.", + }, + { + type: "code_challenge", + exercise: { + prompt: + 'Lee una línea `...`. Si el nodo `stock` falta o no contiene un entero, imprime `STOCK INVALIDO`. Si es entero, crea un `Producto` y muestra `CODIGO STOCK`.', + starterCode: `using System; +using System.Xml; +class Producto { } +class Program { static void Main() { } }`, + solutionCode: `using System; +using System.Xml; +class Producto +{ + public string Codigo{get;private set;} public int Stock{get;private set;} + public Producto(string codigo,int stock){Codigo=codigo;Stock=stock;} +} +class Program +{ + static void Main() + { + XmlDocument doc=new XmlDocument(); + doc.LoadXml(Console.ReadLine()); + XmlElement raiz=doc.DocumentElement; + XmlNode nodo=raiz["stock"]; + int stock; + if(nodo==null || !int.TryParse(nodo.InnerText,out stock)) + { + Console.WriteLine("STOCK INVALIDO"); return; + } + Producto p=new Producto(raiz.GetAttribute("codigo"),stock); + Console.WriteLine(p.Codigo+" "+p.Stock); + } +}`, + hints: [ + "Comprueba `nodo == null` antes de leer `InnerText`.", + "Usa `TryParse`.", + "Construye después de validar.", + ], + difficulty: "medium", + xpReward: 34, + testCases: [ + { + visible: true, + stdin: '8\n', + expectedStdout: "P1 8\n", + }, + { + visible: false, + stdin: 'abc\n', + expectedStdout: "STOCK INVALIDO\n", + }, + { + visible: false, + stdin: 'Sin stock\n', + expectedStdout: "STOCK INVALIDO\n", + }, + ], + }, + }, + ], +}); diff --git a/prisma/content/courses/csharp-poo-1/units/12-xml/lessons/05-persistencia-xml.ts b/prisma/content/courses/csharp-poo-1/units/12-xml/lessons/05-persistencia-xml.ts new file mode 100644 index 0000000..3eac5eb --- /dev/null +++ b/prisma/content/courses/csharp-poo-1/units/12-xml/lessons/05-persistencia-xml.ts @@ -0,0 +1,121 @@ +import { defineLesson } from "../../../../../authoring"; + +export const leccion05 = defineLesson({ + slug: "persistencia-xml", + title: "Persistencia: memoria, archivo y ciclo de vida", + description: + "Entiende qué significa persistir y separa la transformación XML del lugar físico donde se guarda.", + estimatedMinutes: 18, + xpReward: 60, + steps: [ + { + type: "theory", + markdown: `# Persistir significa sobrevivir al proceso + +Una lista y un \`XmlDocument\` en memoria desaparecen cuando termina el programa. **Persistencia** significa guardar información fuera del ciclo de vida del proceso para recuperarla después. + +La misma representación XML puede viajar o guardarse en varios medios: + +- archivo local; +- base de datos; +- mensaje de red; +- almacenamiento remoto. + +Conviene separar dos responsabilidades: + +1. **serializar/deserializar**: objeto ↔ XML; +2. **persistir/recuperar**: XML ↔ medio externo. + +En cpp-ceti podemos probar completamente la primera parte en el navegador. El ejemplo de archivo real se marca como laboratorio local porque el filesystem del runner es temporal y no representa persistencia entre sesiones.`, + }, + { + type: "code_example", + code: `using System; +using System.Xml; +class Program +{ + static void Main() + { + XmlDocument doc = new XmlDocument(); + doc.LoadXml(""); + doc.Save("inventario.xml"); + + XmlDocument recuperado = new XmlDocument(); + recuperado.Load("inventario.xml"); + Console.WriteLine(recuperado.DocumentElement.Name); + } +}`, + explanation: + "`Save` y `Load` muestran persistencia física mediante archivo. El concepto es válido, pero en el juez remoto el archivo sólo vive dentro de esa ejecución.", + runnable: false, + localOnlyNote: + "Ejecuta este laboratorio localmente con Mono/Visual Studio. El runner web no ofrece almacenamiento persistente entre ejecuciones.", + }, + { + type: "matching", + pairs: [ + { left: "Objeto → XML", right: "Serialización." }, + { left: "XML → objeto", right: "Deserialización/parsing." }, + { left: "XML → archivo", right: "Persistencia física." }, + { left: "Archivo → XML", right: "Recuperación." }, + ], + }, + { + type: "code_challenge", + exercise: { + prompt: + "Demuestra un **round-trip en memoria**. Lee `codigo` y `stock`, crea un XML con `XmlDocument`, conviértelo a `OuterXml`, carga ese string en un segundo `XmlDocument` y finalmente imprime `CODIGO STOCK` desde el segundo documento.\n\nNo uses archivos en este reto.", + starterCode: `using System; +using System.Xml; +class Program { static void Main() { } }`, + solutionCode: `using System; +using System.Xml; +class Program +{ + static void Main() + { + string codigo=Console.ReadLine(); + int stock=int.Parse(Console.ReadLine()); + + XmlDocument salida=new XmlDocument(); + XmlElement p=salida.CreateElement("producto"); + p.SetAttribute("codigo",codigo); + XmlElement s=salida.CreateElement("stock"); + s.InnerText=stock.ToString(); + p.AppendChild(s); salida.AppendChild(p); + + string xml=salida.OuterXml; + XmlDocument entrada=new XmlDocument(); + entrada.LoadXml(xml); + + Console.WriteLine(entrada.DocumentElement.GetAttribute("codigo")+" "+entrada.DocumentElement["stock"].InnerText); + } +}`, + hints: [ + "Crea dos instancias de `XmlDocument`.", + "El puente entre ellas es un string XML.", + "Lee el resultado desde el segundo árbol.", + ], + difficulty: "medium", + xpReward: 34, + testCases: [ + { + visible: true, + stdin: "P1\n8\n", + expectedStdout: "P1 8\n", + }, + { + visible: false, + stdin: "X\n0\n", + expectedStdout: "X 0\n", + }, + { + visible: false, + stdin: "A&B\n17\n", + expectedStdout: "A&B 17\n", + }, + ], + }, + }, + ], +}); diff --git a/prisma/content/courses/csharp-poo-1/units/12-xml/practice.ts b/prisma/content/courses/csharp-poo-1/units/12-xml/practice.ts new file mode 100644 index 0000000..f9a74f9 --- /dev/null +++ b/prisma/content/courses/csharp-poo-1/units/12-xml/practice.ts @@ -0,0 +1,130 @@ +import type { PracticeExerciseDefinition } from "../../../../exercises/types"; + +export const practice = [ + { + slug: "csharp-poo2-leer-productos-xml", + title: "Lee un producto XML", + description: "Extrae atributos y nodos desde un documento recibido como texto.", + prompt: + 'lee una línea `......` e imprime `codigo|nombre|precio`.', + starterCode: `using System; +using System.Xml; +class Program { static void Main() { } }`, + solutionCode: `using System;using System.Xml; +class Program{static void Main(){XmlDocument d=new XmlDocument();d.LoadXml(Console.ReadLine());XmlElement p=d.DocumentElement;Console.WriteLine(p.GetAttribute("codigo")+"|"+p["nombre"].InnerText+"|"+p["precio"].InnerText);}}`, + difficulty: "easy", + xpReward: 26, + testCases: [ + { + stdin: 'Mouse250\n', + expectedStdout: "P1|Mouse|250\n", + visible: true, + }, + { + stdin: 'Cable HDMI0\n', + expectedStdout: "X|Cable HDMI|0\n", + visible: false, + }, + { + stdin: 'A & B7\n', + expectedStdout: "Z|A & B|7\n", + visible: false, + }, + ], + }, + { + slug: "csharp-poo2-crear-xml-producto", + title: "Genera XML de una orden", + description: "Construye nodos y atributos con la API XML en lugar de concatenar el documento.", + prompt: + 'lee `folio`, `cliente`, `total` entero y produce exactamente `NT` mediante `XmlDocument`.', + starterCode: `using System; +using System.Xml; +class Program { static void Main() { } }`, + solutionCode: `using System;using System.Xml; +class Program{static void Main(){string f=Console.ReadLine(),c=Console.ReadLine();int t=int.Parse(Console.ReadLine());XmlDocument d=new XmlDocument();XmlElement o=d.CreateElement("orden");o.SetAttribute("folio",f);XmlElement n=d.CreateElement("cliente");n.InnerText=c;o.AppendChild(n);XmlElement x=d.CreateElement("total");x.InnerText=t.ToString();o.AppendChild(x);d.AppendChild(o);Console.WriteLine(d.OuterXml);}}`, + difficulty: "medium", + xpReward: 32, + testCases: [ + { + stdin: "F1\nAna\n300\n", + expectedStdout: 'Ana300\n', + visible: true, + }, + { + stdin: "X\nA & B\n0\n", + expectedStdout: 'A & B0\n', + visible: false, + }, + { + stdin: "Z9\nLuis\n12\n", + expectedStdout: 'Luis12\n', + visible: false, + }, + ], + }, + { + slug: "csharp-poo2-parsear-datos-xml", + title: "Parsing validado", + description: "Rechaza datos tipados incorrectamente antes de crear el objeto de dominio.", + prompt: + 'lee `...`. Si `promedio` no es entero de 0 a 100 imprime `INVALIDO`; si lo es, imprime `registro promedio`.', + starterCode: `using System; +using System.Xml; +class Program { static void Main() { } }`, + solutionCode: `using System;using System.Xml; +class Program{static void Main(){XmlDocument d=new XmlDocument();d.LoadXml(Console.ReadLine());XmlElement a=d.DocumentElement;int p;XmlNode n=a["promedio"];if(n==null||!int.TryParse(n.InnerText,out p)||p<0||p>100){Console.WriteLine("INVALIDO");return;}Console.WriteLine(a.GetAttribute("registro")+" "+p);}}`, + difficulty: "medium", + xpReward: 32, + testCases: [ + { + stdin: '90\n', + expectedStdout: "A1 90\n", + visible: true, + }, + { + stdin: 'abc\n', + expectedStdout: "INVALIDO\n", + visible: false, + }, + { + stdin: '101\n', + expectedStdout: "INVALIDO\n", + visible: false, + }, + ], + }, + { + slug: "csharp-poo2-inventario-xml", + title: "Inventario serializable", + description: "Transforma una lista de productos en un documento XML completo y determinista.", + prompt: + 'lee `n` productos (`codigo`,`stock`) y genera con `XmlDocument` `` con hijos `...` en el mismo orden de entrada. Imprime `OuterXml`.', + starterCode: `using System; +using System.Xml; +class Program { static void Main() { } }`, + solutionCode: `using System;using System.Xml; +class Program{static void Main(){int n=int.Parse(Console.ReadLine());XmlDocument d=new XmlDocument();XmlElement inv=d.CreateElement("inventario");d.AppendChild(inv);for(int i=0;i12\n', + visible: true, + }, + { + stdin: "1\nX\n0\n", + expectedStdout: '0\n', + visible: false, + }, + { + stdin: "3\nA\n5\nB\n0\nC\n9\n", + expectedStdout: + '509\n', + visible: false, + }, + ], + }, +] satisfies PracticeExerciseDefinition[]; diff --git a/prisma/content/courses/csharp-poo-1/units/13-genericos/index.ts b/prisma/content/courses/csharp-poo-1/units/13-genericos/index.ts new file mode 100644 index 0000000..e9a1661 --- /dev/null +++ b/prisma/content/courses/csharp-poo-1/units/13-genericos/index.ts @@ -0,0 +1,20 @@ +import { defineUnit } from "../../../../authoring"; + +import { leccion01 } from "./lessons/01-por-que-genericos"; +import { leccion02 } from "./lessons/02-clase-generica"; +import { leccion03 } from "./lessons/03-metodo-generico"; +import { leccion04 } from "./lessons/04-restricciones-genericas"; +import { leccion05 } from "./lessons/05-repositorio-generico"; + +import { practice } from "./practice"; + +export const csharpPoo2Genericos = defineUnit({ + slug: "csharp-poo2-05-genericos", + title: "Clases y métodos genéricos", + description: + "Reutiliza estructuras y operaciones sin perder type safety, y usa restricciones para expresar qué capacidades necesita un tipo genérico.", + icon: "🧩", + published: false, + lessons: [leccion01, leccion02, leccion03, leccion04, leccion05], + practice, +}); diff --git a/prisma/content/courses/csharp-poo-1/units/13-genericos/lessons/01-por-que-genericos.ts b/prisma/content/courses/csharp-poo-1/units/13-genericos/lessons/01-por-que-genericos.ts new file mode 100644 index 0000000..aec6e4a --- /dev/null +++ b/prisma/content/courses/csharp-poo-1/units/13-genericos/lessons/01-por-que-genericos.ts @@ -0,0 +1,132 @@ +import { defineLesson } from "../../../../../authoring"; + +export const leccion01 = defineLesson({ + slug: "por-que-genericos", + title: "Por qué existen los genéricos", + description: + "Detecta duplicación causada únicamente por el tipo y la reemplaza por una abstracción paramétrica.", + estimatedMinutes: 14, + xpReward: 50, + steps: [ + { + type: "theory", + markdown: `# Reutilizar sin borrar los tipos + +Imagina dos clases casi idénticas: + +- \`CajaProducto\` guarda un \`Producto\`; +- \`CajaCliente\` guarda un \`Cliente\`. + +La única diferencia real es el tipo. Podrías guardar \`object\`, pero entonces cualquier valor entra y el consumidor tiene que hacer casts, perdiendo seguridad de tipos. + +Un **genérico** conserva la forma de la clase y deja el tipo como parámetro: + +\`\`\`csharp +class Caja +{ + private T valor; +} +\`\`\` + +Cuando escribes \`Caja\`, \`T\` significa \`Producto\`; en \`Caja\`, significa \`Cliente\`. + +La reutilización ocurre en compilación: el código sigue sabiendo qué tipo entra y sale.`, + }, + { + type: "code_example", + code: `using System; + +class Caja +{ + private T valor; + public Caja(T valor) { this.valor = valor; } + public T Obtener() { return valor; } +} + +class Program +{ + static void Main() + { + Caja numero = new Caja(7); + Caja texto = new Caja("CETI"); + + Console.WriteLine(numero.Obtener()); + Console.WriteLine(texto.Obtener()); + } +}`, + explanation: + "la misma implementación trabaja con dos tipos y devuelve exactamente el tipo configurado en cada instancia.", + runnable: true, + expectedOutput: `7 +CETI`, + }, + { + type: "quiz", + question: + "¿qué ventaja tiene `Caja` frente a una caja que guarda `object`?", + options: [ + "Puede guardar absolutamente cualquier valor sin reglas.", + "El compilador conserva que entra y sale un `Producto`, evitando casts accidentales.", + "Convierte automáticamente Producto en string.", + "Crea una base de datos.", + ], + correctIndex: 1, + explanation: + "El compilador conserva que entra y sale un `Producto`, evitando casts accidentales.", + }, + { + type: "code_challenge", + exercise: { + prompt: + "Implementa `Caja` con constructor y método `Obtener()`. Lee una línea de texto y un entero, crea `Caja` y `Caja`, e imprime ambos valores en ese orden.", + starterCode: `using System; +class Caja +{ + // completa +} +class Program { static void Main() { } }`, + solutionCode: `using System; +class Caja +{ + private T valor; + public Caja(T valor) { this.valor = valor; } + public T Obtener() { return valor; } +} +class Program +{ + static void Main() + { + Caja texto = new Caja(Console.ReadLine()); + Caja numero = new Caja(int.Parse(Console.ReadLine())); + Console.WriteLine(texto.Obtener()); + Console.WriteLine(numero.Obtener()); + } +}`, + hints: [ + "`T` puede usarse como tipo de campo, parámetro y retorno.", + "no uses `object`.", + "instancia indicando el tipo entre `< >`.", + ], + difficulty: "easy", + xpReward: 26, + testCases: [ + { + visible: true, + stdin: "Hola\n7\n", + expectedStdout: "Hola\n7\n", + }, + { + visible: false, + stdin: "Cable HDMI\n0\n", + expectedStdout: "Cable HDMI\n0\n", + }, + { + visible: false, + stdin: "X\n-5\n", + expectedStdout: "X\n-5\n", + }, + ], + }, + }, + ], +}); diff --git a/prisma/content/courses/csharp-poo-1/units/13-genericos/lessons/02-clase-generica.ts b/prisma/content/courses/csharp-poo-1/units/13-genericos/lessons/02-clase-generica.ts new file mode 100644 index 0000000..271995b --- /dev/null +++ b/prisma/content/courses/csharp-poo-1/units/13-genericos/lessons/02-clase-generica.ts @@ -0,0 +1,125 @@ +import { defineLesson } from "../../../../../authoring"; + +export const leccion02 = defineLesson({ + slug: "clase-generica", + title: "Una clase genérica que organiza muchos objetos", + description: + "Combina genéricos con `List` para construir un contenedor reutilizable sin duplicar repositorios por entidad.", + estimatedMinutes: 16, + xpReward: 55, + steps: [ + { + type: "theory", + markdown: `# El tipo genérico puede atravesar toda la clase + +Un \`Repositorio\` puede guardar una \`List\`, recibir \`T\` en \`Agregar\` y devolver \`T\` al consultar. La clase no necesita conocer \`Producto\`, \`Cliente\` o \`Alumno\`. + +Eso no significa que todos los repositorios deban ser iguales. El genérico sirve cuando las operaciones son realmente comunes. + +En esta primera versión las operaciones son neutrales: + +- agregar un elemento; +- consultar por posición; +- conocer la cantidad. + +Más adelante una restricción permitirá pedir capacidades específicas a \`T\`.`, + }, + { + type: "code_example", + code: `using System; +using System.Collections.Generic; + +class Repositorio +{ + private List elementos = new List(); + + public void Agregar(T elemento) { elementos.Add(elemento); } + public T Obtener(int indice) { return elementos[indice]; } + public int Cantidad() { return elementos.Count; } +} + +class Program +{ + static void Main() + { + Repositorio nombres = new Repositorio(); + nombres.Agregar("Ana"); + nombres.Agregar("Luis"); + Console.WriteLine(nombres.Cantidad()); + Console.WriteLine(nombres.Obtener(1)); + } +}`, + explanation: + "`Repositorio` convierte todas las apariciones de `T` en `string`; otro repositorio puede usar otro tipo sin copiar la clase.", + runnable: true, + expectedOutput: `2 +Luis`, + }, + { + type: "fill_blank", + template: `class Repositorio<{{0}}> +{ + private List<{{1}}> datos = new List(); + public void Agregar({{2}} item) { datos.Add(item); } + public T Obtener(int i) { return datos[i]; } +}`, + blanks: [{ answer: "T" }, { answer: "T" }, { answer: "T" }], + }, + { + type: "code_challenge", + exercise: { + prompt: + "Implementa `Repositorio` con `Agregar(T)`, `Obtener(int)` y `Cantidad()`. Lee `n` enteros, agrégalos a `Repositorio`, después lee un índice válido e imprime primero la cantidad y luego el valor de esa posición.", + starterCode: `using System; +using System.Collections.Generic; +class Repositorio { } +class Program { static void Main() { } }`, + solutionCode: `using System; +using System.Collections.Generic; +class Repositorio +{ + private List datos = new List(); + public void Agregar(T item) { datos.Add(item); } + public T Obtener(int i) { return datos[i]; } + public int Cantidad() { return datos.Count; } +} +class Program +{ + static void Main() + { + int n=int.Parse(Console.ReadLine()); + Repositorio repo=new Repositorio(); + for(int i=0;i` es válida dentro de otra clase genérica.", + "`Obtener` retorna `T`.", + "la instancia concreta es `Repositorio`.", + ], + difficulty: "medium", + xpReward: 32, + testCases: [ + { + visible: true, + stdin: "3\n10\n20\n30\n1\n", + expectedStdout: "3\n20\n", + }, + { + visible: false, + stdin: "1\n-4\n0\n", + expectedStdout: "1\n-4\n", + }, + { + visible: false, + stdin: "4\n7\n8\n9\n10\n3\n", + expectedStdout: "4\n10\n", + }, + ], + }, + }, + ], +}); diff --git a/prisma/content/courses/csharp-poo-1/units/13-genericos/lessons/03-metodo-generico.ts b/prisma/content/courses/csharp-poo-1/units/13-genericos/lessons/03-metodo-generico.ts new file mode 100644 index 0000000..14680a8 --- /dev/null +++ b/prisma/content/courses/csharp-poo-1/units/13-genericos/lessons/03-metodo-generico.ts @@ -0,0 +1,109 @@ +import { defineLesson } from "../../../../../authoring"; + +export const leccion03 = defineLesson({ + slug: "metodo-generico", + title: "Métodos genéricos", + description: + "Parametriza una operación puntual sin volver genérica toda la clase que la contiene.", + estimatedMinutes: 15, + xpReward: 50, + steps: [ + { + type: "theory", + markdown: `# A veces la reutilización pertenece al método + +No necesitas \`class Utilidades\` si sólo una operación depende del tipo. Un **método genérico** declara su propio parámetro: + +\`\`\`csharp +static void Intercambiar(ref T a, ref T b) +\`\`\` + +La clase que contiene el método puede seguir siendo normal. Al llamar, C# suele inferir \`T\` desde los argumentos. + +\`ref\` permite que el método reemplace las variables del llamador. Aquí se usa porque intercambiar exige modificar ambas referencias/valores originales; no es una propiedad especial de los genéricos.`, + }, + { + type: "code_example", + code: `using System; +class Program +{ + static void Intercambiar(ref T a, ref T b) + { + T temp=a; a=b; b=temp; + } + static void Main() + { + string a="uno", b="dos"; + Intercambiar(ref a,ref b); + Console.WriteLine(a+" "+b); + + int x=3,y=8; + Intercambiar(ref x,ref y); + Console.WriteLine(x+" "+y); + } +}`, + explanation: + "una sola operación funciona con texto y enteros; `T temp` conserva exactamente el tipo de la llamada.", + runnable: true, + expectedOutput: `dos uno +8 3`, + }, + { + type: "code_completion", + lines: ["T temp = a;", "a = b;", "b = temp;"], + }, + { + type: "code_challenge", + exercise: { + prompt: + "Escribe `static T Primero(List datos)` que devuelva el primer elemento. Lee dos strings y dos enteros, crea una lista de cada tipo e imprime el primer elemento de ambas usando el mismo método genérico.", + starterCode: `using System; +using System.Collections.Generic; +class Program +{ + static T Primero(List datos) { /* completa */ } + static void Main() { } +}`, + solutionCode: `using System; +using System.Collections.Generic; +class Program +{ + static T Primero(List datos) { return datos[0]; } + static void Main() + { + List textos=new List(); + textos.Add(Console.ReadLine()); textos.Add(Console.ReadLine()); + List numeros=new List(); + numeros.Add(int.Parse(Console.ReadLine())); numeros.Add(int.Parse(Console.ReadLine())); + Console.WriteLine(Primero(textos)); + Console.WriteLine(Primero(numeros)); + } +}`, + hints: [ + "el retorno es `T`.", + "la lista del parámetro también usa `T`.", + "no conviertas a `object` ni a `string`.", + ], + difficulty: "easy", + xpReward: 26, + testCases: [ + { + visible: true, + stdin: "A\nB\n3\n4\n", + expectedStdout: "A\n3\n", + }, + { + visible: false, + stdin: "Cable HDMI\nX\n0\n9\n", + expectedStdout: "Cable HDMI\n0\n", + }, + { + visible: false, + stdin: "Z\nY\n-2\n-1\n", + expectedStdout: "Z\n-2\n", + }, + ], + }, + }, + ], +}); diff --git a/prisma/content/courses/csharp-poo-1/units/13-genericos/lessons/04-restricciones-genericas.ts b/prisma/content/courses/csharp-poo-1/units/13-genericos/lessons/04-restricciones-genericas.ts new file mode 100644 index 0000000..aa7fdf2 --- /dev/null +++ b/prisma/content/courses/csharp-poo-1/units/13-genericos/lessons/04-restricciones-genericas.ts @@ -0,0 +1,143 @@ +import { defineLesson } from "../../../../../authoring"; + +export const leccion04 = defineLesson({ + slug: "restricciones-genericas", + title: "Restricciones: qué exige el genérico a T", + description: + "Usa `where` para expresar capacidades mínimas y obtener seguridad de tipos dentro de una clase genérica.", + estimatedMinutes: 17, + xpReward: 60, + steps: [ + { + type: "theory", + markdown: `# Un genérico sin restricciones sabe muy poco + +Dentro de \`Repositorio\`, el compilador no puede asumir que \`T\` tiene una propiedad \`Id\`, un constructor concreto o una clase base específica. + +Una **restricción** declara qué tipos son aceptables. Ejemplos: + +- \`where T : class\` → debe ser tipo de referencia; +- \`where T : Entidad\` → debe heredar de \`Entidad\`; +- \`where T : new()\` → debe tener constructor público sin parámetros. + +En este curso usaremos una clase base porque ya conoces herencia. \`where T : Entidad\` permite que el repositorio use los miembros públicos definidos por \`Entidad\` sin cast. + +La restricción no es decoración: convierte una expectativa implícita en contrato de compilación.`, + }, + { + type: "code_example", + code: `using System; +using System.Collections.Generic; +class Entidad +{ + public string Id{get;private set;} + public Entidad(string id){Id=id;} +} +class Producto:Entidad +{ + public Producto(string id):base(id){} +} +class Repositorio where T : Entidad +{ + private List datos=new List(); + public void Agregar(T item){datos.Add(item);} + public T Buscar(string id) + { + for(int i=0;i r=new Repositorio(); + r.Agregar(new Producto("P1")); + Console.WriteLine(r.Buscar("P1").Id); + } +}`, + explanation: + "la restricción garantiza que cualquier `T` tiene `Id`; por eso `Buscar` puede usarlo sin saber la clase concreta.", + runnable: true, + expectedOutput: `P1`, + }, + { + type: "quiz", + question: "¿qué impide `where T : Entidad`?", + options: [ + "Crear `Repositorio` si `Producto : Entidad`.", + "Crear `Repositorio`, porque `int` no deriva de `Entidad`.", + "Usar `Id` dentro del repositorio.", + "Crear varias instancias del repositorio.", + ], + correctIndex: 1, + explanation: + "Crear `Repositorio`, porque `int` no deriva de `Entidad`.", + }, + { + type: "code_challenge", + exercise: { + prompt: + "Crea base `Entidad` con propiedad pública `Id` y constructor. Crea `Alumno : Entidad` con `Nombre`. Implementa `Repositorio where T : Entidad` con `Agregar` y `Buscar(id)`.\n\nLee `n` alumnos (`id`,`nombre`) y luego un id. Imprime el nombre o `NO`.", + starterCode: `using System; +using System.Collections.Generic; +class Entidad { } +class Alumno : Entidad { } +class Repositorio where T : Entidad { } +class Program { static void Main() { } }`, + solutionCode: `using System; +using System.Collections.Generic; +class Entidad +{ + public string Id{get;private set;} + public Entidad(string id){Id=id;} +} +class Alumno:Entidad +{ + public string Nombre{get;private set;} + public Alumno(string id,string nombre):base(id){Nombre=nombre;} +} +class Repositorio where T : Entidad +{ + private List datos=new List(); + public void Agregar(T x){datos.Add(x);} + public T Buscar(string id){for(int i=0;i r=new Repositorio(); + for(int i=0;i\` puede encargarse de almacenamiento en memoria y búsqueda por identidad. \`Producto\` y \`Cliente\` siguen teniendo reglas distintas. + +Ese es el uso sano del genérico: reutilizar una **mecánica común** sin fingir que todas las entidades son iguales. + +La clase base \`Entidad\` sólo contiene el contrato compartido (\`Id\`). El repositorio no debería empezar a saber de \`Stock\`, \`Correo\` o reglas particulares. + +Más adelante la persistencia XML podrá recibir el mismo tipo genérico y guardar distintos bienes del inventario, que es precisamente uno de los productos parciales del programa oficial.`, + }, + { + type: "code_example", + code: `using System; +using System.Collections.Generic; +class Entidad{public string Id{get;private set;}public Entidad(string id){Id=id;}} +class Producto:Entidad{public int Stock{get;private set;}public Producto(string id,int stock):base(id){Stock=stock;}} +class Cliente:Entidad{public string Nombre{get;private set;}public Cliente(string id,string nombre):base(id){Nombre=nombre;}} +class Repositorio where T:Entidad +{ + private List datos=new List(); + public void Agregar(T x){datos.Add(x);} + public T Buscar(string id){for(int i=0;i rp=new Repositorio(); rp.Agregar(new Producto("P1",5)); + Repositorio rc=new Repositorio(); rc.Agregar(new Cliente("C1","Ana")); + Console.WriteLine(rp.Buscar("P1").Stock); + Console.WriteLine(rc.Buscar("C1").Nombre); + } +}`, + explanation: + "la infraestructura de búsqueda es común; el estado específico sigue en cada clase concreta.", + runnable: true, + expectedOutput: `5 +Ana`, + }, + { + type: "matching", + pairs: [ + { left: "Entidad.Id", right: "Contrato mínimo compartido." }, + { + left: "Repositorio", + right: "Mecánica reusable de almacenamiento/búsqueda.", + }, + { left: "Producto.Stock", right: "Regla/dato específico de Producto." }, + { left: "Cliente.Nombre", right: "Dato específico de Cliente." }, + ], + }, + { + type: "code_challenge", + exercise: { + prompt: + "Implementa `Entidad`, `Producto`, `Cliente` y `Repositorio where T : Entidad`. Carga un producto y un cliente desde entrada. Después lee dos ids de consulta y muestra `PRODUCTO stock|NO` y `CLIENTE nombre|NO`.", + starterCode: `using System; +using System.Collections.Generic; +class Entidad { } +class Producto : Entidad { } +class Cliente : Entidad { } +class Repositorio where T : Entidad { } +class Program { static void Main() { } }`, + solutionCode: `using System; +using System.Collections.Generic; +class Entidad{public string Id{get;private set;}public Entidad(string id){Id=id;}} +class Producto:Entidad{public int Stock{get;private set;}public Producto(string id,int stock):base(id){Stock=stock;}} +class Cliente:Entidad{public string Nombre{get;private set;}public Cliente(string id,string nombre):base(id){Nombre=nombre;}} +class Repositorio where T:Entidad +{ + private List datos=new List(); + public void Agregar(T x){datos.Add(x);} + public T Buscar(string id){for(int i=0;i rp=new Repositorio(); + rp.Agregar(new Producto(Console.ReadLine(),int.Parse(Console.ReadLine()))); + Repositorio rc=new Repositorio(); + rc.Agregar(new Cliente(Console.ReadLine(),Console.ReadLine())); + Producto p=rp.Buscar(Console.ReadLine()); Cliente c=rc.Buscar(Console.ReadLine()); + Console.WriteLine(p==null?"PRODUCTO NO":"PRODUCTO "+p.Stock); + Console.WriteLine(c==null?"CLIENTE NO":"CLIENTE "+c.Nombre); + } +}`, + hints: [ + "crea dos repositorios con distintos `T`.", + "la clase genérica no necesita `if` por tipo.", + "cada resultado conserva su tipo concreto.", + ], + difficulty: "hard", + xpReward: 42, + testCases: [ + { + visible: true, + stdin: "P1\n5\nC1\nAna\nP1\nC1\n", + expectedStdout: "PRODUCTO 5\nCLIENTE Ana\n", + }, + { + visible: false, + stdin: "P\n0\nC\nLuis\nX\nC\n", + expectedStdout: "PRODUCTO NO\nCLIENTE Luis\n", + }, + { + visible: false, + stdin: "A\n9\nB\nSol\nA\nX\n", + expectedStdout: "PRODUCTO 9\nCLIENTE NO\n", + }, + ], + }, + }, + ], +}); diff --git a/prisma/content/courses/csharp-poo-1/units/13-genericos/practice.ts b/prisma/content/courses/csharp-poo-1/units/13-genericos/practice.ts new file mode 100644 index 0000000..837de42 --- /dev/null +++ b/prisma/content/courses/csharp-poo-1/units/13-genericos/practice.ts @@ -0,0 +1,124 @@ +import type { PracticeExerciseDefinition } from "../../../../exercises/types"; + +export const practice = [ + { + slug: "csharp-poo2-caja-generica", + title: "Caja genérica", + description: "Implementa un contenedor type-safe reutilizado con dos tipos.", + prompt: + "crea `Caja` con `Guardar(T)` y `Obtener()`. Lee string e int, usa dos cajas e imprime ambos.", + starterCode: `using System; +class Caja { } +class Program { static void Main() { } }`, + solutionCode: `using System; +class Caja{private T valor;public void Guardar(T v){valor=v;}public T Obtener(){return valor;}} +class Program{static void Main(){Caja a=new Caja();a.Guardar(Console.ReadLine());Cajab=new Caja();b.Guardar(int.Parse(Console.ReadLine()));Console.WriteLine(a.Obtener());Console.WriteLine(b.Obtener());}}`, + difficulty: "easy", + xpReward: 24, + testCases: [ + { stdin: "Hola\n3\n", expectedStdout: "Hola\n3\n", visible: true }, + { stdin: "X Y\n0\n", expectedStdout: "X Y\n0\n", visible: false }, + { stdin: "Z\n-1\n", expectedStdout: "Z\n-1\n", visible: false }, + ], + }, + { + slug: "csharp-poo2-repositorio-productos", + title: "Repositorio de productos", + description: + "Reutiliza `Repositorio` para almacenar una colección de objetos tipados.", + prompt: + "implementa `Repositorio` con `Agregar`, `Cantidad`, `Obtener`. Lee `n` productos (`codigo`,`stock`), agrega objetos y consulta un índice. Imprime `cantidad` y `codigo stock`.", + starterCode: `using System; +using System.Collections.Generic; +class Producto { } +class Repositorio { } +class Program { static void Main() { } }`, + solutionCode: `using System;using System.Collections.Generic; +class Producto{public string Codigo{get;private set;}public int Stock{get;private set;}public Producto(string c,int s){Codigo=c;Stock=s;}} +class Repositorio{private Listd=new List();public void Agregar(T x){d.Add(x);}public int Cantidad(){return d.Count;}public T Obtener(int i){return d[i];}} +class Program{static void Main(){int n=int.Parse(Console.ReadLine());Repositorior=new Repositorio();for(int i=0;i(List)`. Lee tres strings y tres ints; imprime el primer elemento de cada lista.", + starterCode: `using System; +using System.Collections.Generic; +class Program { static T Primero(List x) { } static void Main() { } }`, + solutionCode: `using System;using System.Collections.Generic; +class Program{static T Primero(Listx){return x[0];}static void Main(){Lista=new List();for(int i=0;i<3;i++)a.Add(Console.ReadLine());Listb=new List();for(int i=0;i<3;i++)b.Add(int.Parse(Console.ReadLine()));Console.WriteLine(Primero(a));Console.WriteLine(Primero(b));}}`, + difficulty: "easy", + xpReward: 24, + testCases: [ + { stdin: "A\nB\nC\n1\n2\n3\n", expectedStdout: "A\n1\n", visible: true }, + { + stdin: "X Y\nZ\nQ\n0\n9\n8\n", + expectedStdout: "X Y\n0\n", + visible: false, + }, + { + stdin: "uno\ndos\ntres\n-2\n0\n2\n", + expectedStdout: "uno\n-2\n", + visible: false, + }, + ], + }, + { + slug: "csharp-poo2-repositorio-con-restriccion", + title: "Repositorio restringido", + description: "Usa una clase base como contrato para buscar cualquier entidad por ID.", + prompt: + "crea `Entidad(Id)`, `Bien : Entidad (Nombre)` y `Repositorio where T : Entidad` con `Agregar`/`Buscar`. Lee `n` bienes y una consulta; imprime nombre o `NO`.", + starterCode: `using System; +using System.Collections.Generic; +class Entidad { } +class Bien : Entidad { } +class Repositorio where T : Entidad { } +class Program { static void Main() { } }`, + solutionCode: `using System;using System.Collections.Generic; +class Entidad{public string Id{get;private set;}public Entidad(string id){Id=id;}} +class Bien:Entidad{public string Nombre{get;private set;}public Bien(string id,string n):base(id){Nombre=n;}} +class Repositorio where T:Entidad{private Listd=new List();public void Agregar(T x){d.Add(x);}public T Buscar(string id){for(int i=0;ir=new Repositorio();for(int i=0;i=1 && puerto<=65535; + Console.WriteLine(ipValida && puertoValido ? "Endpoint valido" : "Endpoint invalido"); + } +}`, + explanation: + "validar la configuración no abre red; sólo comprueba que los valores pueden representar un endpoint básico.", + runnable: true, + expectedOutput: "Endpoint valido", + }, + { + type: "fill_blank", + template: `IPAddress ip; +bool okIp = IPAddress.{{0}}(textoIp, out ip); +bool okPuerto = puerto >= {{1}} && puerto <= {{2}};`, + blanks: [ + { answer: "TryParse" }, + { answer: "1" }, + { answer: "65535" }, + ], + }, + { + type: "code_challenge", + exercise: { + prompt: + "Lee una dirección IP y un entero `puerto`. Imprime `VALIDO` sólo si `IPAddress.TryParse` acepta la IP y el puerto está entre 1 y 65535; en otro caso `INVALIDO`. No abras sockets.", + starterCode: `using System; +using System.Net; +class Program { static void Main() { } }`, + solutionCode: `using System; +using System.Net; +class Program +{ + static void Main() + { + string texto=Console.ReadLine(); int puerto=int.Parse(Console.ReadLine()); + IPAddress ip; bool ok=IPAddress.TryParse(texto,out ip) && puerto>=1 && puerto<=65535; + Console.WriteLine(ok?"VALIDO":"INVALIDO"); + } +}`, + hints: [ + "TryParse no lanza por formato inválido", + "valida IP y puerto con &&", + "no intentes conectarte.", + ], + difficulty: "easy", + xpReward: 26, + testCases: [ + { + visible: true, + stdin: "127.0.0.1\n5050\n", + expectedStdout: "VALIDO\n", + }, + { + visible: false, + stdin: "999.1.1.1\n5050\n", + expectedStdout: "INVALIDO\n", + }, + { + visible: false, + stdin: "192.168.0.5\n70000\n", + expectedStdout: "INVALIDO\n", + }, + ], + }, + }, + ], +}); diff --git a/prisma/content/courses/csharp-poo-1/units/15-redes/lessons/04-cliente-servidor-tcp.ts b/prisma/content/courses/csharp-poo-1/units/15-redes/lessons/04-cliente-servidor-tcp.ts new file mode 100644 index 0000000..f9833fc --- /dev/null +++ b/prisma/content/courses/csharp-poo-1/units/15-redes/lessons/04-cliente-servidor-tcp.ts @@ -0,0 +1,137 @@ +import { defineLesson } from "../../../../../authoring"; + +export const leccion04 = defineLesson({ + slug: "cliente-servidor-tcp", + title: "Arquitectura cliente-servidor TCP", + description: + "Sigue el ciclo de escuchar, aceptar, conectar, enviar y responder, y separa ese transporte del servicio de dominio.", + estimatedMinutes: 18, + xpReward: 65, + steps: [ + { + type: "theory", + markdown: `# Un servidor ofrece un servicio en un endpoint + +En un modelo cliente-servidor TCP básico: + +**Servidor** +1. crea un listener en IP/puerto; +2. comienza a escuchar; +3. acepta una conexión; +4. lee una solicitud; +5. ejecuta una operación de aplicación; +6. escribe una respuesta; +7. cierra recursos cuando corresponde. + +**Cliente** +1. conoce el endpoint del servidor; +2. conecta; +3. envía una solicitud del protocolo; +4. lee la respuesta. + +El socket no debe contener reglas de inventario. Idealmente entrega el mensaje a una clase como \`ProcesadorComandos\`, que puede probarse sin red.`, + }, + { + type: "code_example", + code: `// Servidor.cs +using System; +using System.IO; +using System.Net; +using System.Net.Sockets; +class Servidor +{ + static void Main() + { + TcpListener listener=new TcpListener(IPAddress.Loopback,5050); + listener.Start(); + using(TcpClient cliente=listener.AcceptTcpClient()) + using(NetworkStream red=cliente.GetStream()) + using(StreamReader entrada=new StreamReader(red)) + using(StreamWriter salida=new StreamWriter(red){AutoFlush=true}) + { + string mensaje=entrada.ReadLine(); + salida.WriteLine("RECIBIDO|"+mensaje); + } + listener.Stop(); + } +} + +// Cliente.cs (ejecutar en otro proceso) +// TcpClient cliente=new TcpClient("127.0.0.1",5050); +// escribir una línea y leer la respuesta usando su NetworkStream.`, + explanation: + "el ejemplo usa una línea como framing mínimo. Sólo demuestra el ciclo TCP local; no es arquitectura de producción.", + runnable: false, + localOnlyNote: + "Requiere dos procesos locales y apertura de un puerto TCP. Ejecuta Servidor.cs y Cliente.cs en localhost fuera del runner web.", + }, + { + type: "code_completion", + lines: [ + "listener.Start();", + "TcpClient cliente = listener.AcceptTcpClient();", + "NetworkStream red = cliente.GetStream();", + "leer solicitud;", + "producir respuesta;", + "cerrar/Dispose y listener.Stop().", + ], + prompt: "Orden del servidor:", + }, + { + type: "code_challenge", + exercise: { + prompt: + "Implementa sólo la **lógica del protocolo**, no la red. Lee `n` solicitudes con formato `PING` o `ECO|texto`. Para cada una imprime la respuesta:\n\n- `PING` → `PONG`\n- `ECO|texto` → `OK|texto`\n- cualquier otra → `ERROR|COMANDO`", + starterCode: `using System; +class Procesador +{ + public string Responder(string solicitud) { return ""; } +} +class Program { static void Main() { } }`, + solutionCode: `using System; +class Procesador +{ + public string Responder(string solicitud) + { + string[] p=solicitud.Split('|'); + if(p[0]=="PING" && p.Length==1)return "PONG"; + if(p[0]=="ECO" && p.Length==2)return "OK|"+p[1]; + return "ERROR|COMANDO"; + } +} +class Program +{ + static void Main() + { + int n=int.Parse(Console.ReadLine()); Procesador p=new Procesador(); + for(int i=0;i100)return false; + string[] p=mensaje.Split('|'); + if(p.Length!=3 || p[0]!="ALTA" || p[1].Length==0)return false; + int stock; + return int.TryParse(p[2],out stock) && stock>=0; + } +} +class Program +{ + static void Main() + { + Console.WriteLine(Validador.AltaValida("ALTA|P1|5")); + Console.WriteLine(Validador.AltaValida("ALTA|P1|-2")); + } +}`, + explanation: + "la aplicación sólo entrega al dominio datos cuya forma y tipos cumplen el contrato mínimo.", + runnable: true, + expectedOutput: `True +False`, + }, + { + type: "matching", + pairs: [ + { left: "Mensaje > límite", right: "Rechazar antes de parsear más." }, + { left: "Comando desconocido", right: "Respuesta de protocolo `ERROR`." }, + { left: "Stock no entero", right: "Error de validación de campo." }, + { + left: "`ALTA|P1|-3`", + right: "Estructura correcta, dato de dominio inválido.", + }, + ], + }, + { + type: "code_challenge", + exercise: { + prompt: + "Lee `n` mensajes. Considera válido sólo `ALTA|CODIGO|STOCK` donde:\n\n- longitud total <= 50;\n- exactamente 3 campos;\n- código no vacío;\n- stock es entero entre 0 y 100000.\n\nPor cada línea imprime `VALIDO` o `INVALIDO`.", + starterCode: `using System; +class Program +{ + static bool EsValido(string mensaje) { return false; } + static void Main() { } +}`, + solutionCode: `using System; +class Program +{ + static bool EsValido(string mensaje) + { + if(mensaje==null || mensaje.Length>50)return false; + string[] p=mensaje.Split('|'); + if(p.Length!=3 || p[0]!="ALTA" || p[1].Length==0)return false; + int stock; + return int.TryParse(p[2],out stock) && stock>=0 && stock<=100000; + } + static void Main() + { + int n=int.Parse(Console.ReadLine()); + for(int i=0;i0&&int.TryParse(p[2],out s))Console.WriteLine(p[1]+" "+s);else Console.WriteLine("ERROR");}}`, + difficulty: "medium", + xpReward: 30, + testCases: [ + { stdin: "OK|P1|7\n", expectedStdout: "P1 7\n", visible: true }, + { stdin: "OK|P1|x\n", expectedStdout: "ERROR\n", visible: false }, + { stdin: "ERROR|NO\n", expectedStdout: "ERROR\n", visible: false }, + ], + }, + { + slug: "csharp-poo2-validar-endpoint", + title: "Valida endpoint", + description: "Rechaza configuración IP/puerto inválida antes de abrir red.", + prompt: "lee `ip` y `puerto`; imprime `ip:puerto` si ambos son válidos, o `INVALIDO`.", + starterCode: `using System; +using System.Net; +class Program { static void Main() { } }`, + solutionCode: `using System;using System.Net; +class Program{static void Main(){string t=Console.ReadLine();int p=int.Parse(Console.ReadLine());IPAddress ip;if(IPAddress.TryParse(t,out ip)&&p>=1&&p<=65535)Console.WriteLine(t+":"+p);else Console.WriteLine("INVALIDO");}}`, + difficulty: "easy", + xpReward: 26, + testCases: [ + { stdin: "10.0.0.1\n80\n", expectedStdout: "10.0.0.1:80\n", visible: true }, + { stdin: "bad\n80\n", expectedStdout: "INVALIDO\n", visible: false }, + { stdin: "127.0.0.1\n0\n", expectedStdout: "INVALIDO\n", visible: false }, + ], + }, + { + slug: "csharp-poo2-procesar-comando-remoto", + title: "Procesa un comando remoto sin red", + description: "Mantiene la lógica del servicio independiente del transporte de sockets.", + prompt: + "empieza con diccionario vacío y procesa `n` mensajes: `ALTA|codigo|stock`, `BUSCAR|codigo`, `ELIMINAR|codigo`. Respuestas: alta nueva `OK`, duplicada `DUP`; buscar imprime stock o `NO`; eliminar no imprime. Rechaza cualquier mensaje mal formado con `ERROR`.", + starterCode: `using System; +using System.Collections.Generic; +class Procesador { } +class Program { static void Main() { } }`, + solutionCode: `using System;using System.Collections.Generic; +class Procesador +{ + private Dictionaryd=new Dictionary(); + public string Procesar(string m) + { + string[] p=m.Split('|'); + if(p.Length==3&&p[0]=="ALTA") + { + int s;if(p[1].Length==0||!int.TryParse(p[2],out s)||s<0)return "ERROR"; + if(d.ContainsKey(p[1]))return "DUP";d.Add(p[1],s);return "OK"; + } + if(p.Length==2&&p[0]=="BUSCAR"){int s;return d.TryGetValue(p[1],out s)?s.ToString():"NO";} + if(p.Length==2&&p[0]=="ELIMINAR"){d.Remove(p[1]);return null;} + return "ERROR"; + } +} +class Program{static void Main(){int n=int.Parse(Console.ReadLine());Procesador p=new Procesador();for(int i=0;i\`: mecánica reusable de almacenamiento; +- \`XmlInventario\`: transformación XML; +- \`ProcesadorComandos\`: protocolo de aplicación. + +La red será un adaptador alrededor del procesador, no el lugar donde vive el negocio.`, + }, + { + type: "matching", + pairs: [ + { + left: "Bien", + right: "Entidad del dominio con código, nombre y stock.", + }, + { + left: "Inventario", + right: "Coordina reglas y operaciones sobre bienes.", + }, + { + left: "Repositorio", + right: "Almacenamiento reusable de entidades.", + }, + { + left: "XmlInventario", + right: "Traducción entre objetos y XML.", + }, + { + left: "ProcesadorComandos", + right: "Traduce mensajes del protocolo a operaciones del inventario.", + }, + { + left: "TcpClient/TcpListener", + right: "Transporte local; no contiene reglas de stock.", + }, + ], + }, + { + type: "code_example", + code: `using System; +using System.Collections.Generic; + +class Bien +{ + public string Codigo{get;private set;} + public string Nombre{get;private set;} + public int Stock{get;private set;} + public Bien(string codigo,string nombre,int stock) + { + if(stock<0)throw new ArgumentException("Stock invalido"); + Codigo=codigo;Nombre=nombre;Stock=stock; + } + public void Ajustar(int cambio) + { + if(Stock+cambio<0)throw new ArgumentException("Stock insuficiente"); + Stock+=cambio; + } +} + +class Area +{ + public string Nombre{get;private set;} + private List bienes=new List(); + public Area(string nombre){Nombre=nombre;} + public void Agregar(Bien bien){bienes.Add(bien);} +}`, + explanation: + "el modelo expresa identidad, invariantes y relación de composición antes de introducir XML o sockets.", + runnable: false, + localOnlyNote: + "Fragmento de modelo sin Main. Se usa para leer/dibujar UML y luego se integra en los retos ejecutables.", + }, + { + type: "code_challenge", + exercise: { + prompt: + 'Implementa `Bien` con `Codigo`, `Nombre`, `Stock`, constructor que rechace stock negativo con `ArgumentException("Stock invalido")` y método `Ajustar(int cambio)` que rechace un resultado negativo con `ArgumentException("Stock insuficiente")`.\n\nLee código, nombre, stock inicial y cambio. Imprime `CODIGO NOMBRE STOCK` o `ERROR: mensaje`.', + starterCode: `using System; +class Bien { } +class Program { static void Main() { } }`, + solutionCode: `using System; +class Bien +{ + public string Codigo{get;private set;} + public string Nombre{get;private set;} + public int Stock{get;private set;} + public Bien(string codigo,string nombre,int stock) + { + if(stock<0)throw new ArgumentException("Stock invalido"); + Codigo=codigo;Nombre=nombre;Stock=stock; + } + public void Ajustar(int cambio) + { + if(Stock+cambio<0)throw new ArgumentException("Stock insuficiente"); + Stock+=cambio; + } +} +class Program +{ + static void Main() + { + try + { + Bien b=new Bien(Console.ReadLine(),Console.ReadLine(),int.Parse(Console.ReadLine())); + b.Ajustar(int.Parse(Console.ReadLine())); + Console.WriteLine(b.Codigo+" "+b.Nombre+" "+b.Stock); + } + catch(ArgumentException ex){Console.WriteLine("ERROR: "+ex.Message);} + } +}`, + hints: [ + "valida en el objeto", + "no permitas que `Program` modifique Stock directamente", + "el cambio puede ser negativo si el resultado sigue siendo válido", + ], + difficulty: "medium", + xpReward: 34, + structure: { + classes: [ + { + name: "Bien", + properties: [ + { name: "Codigo", visibility: "public", type: "string" }, + { name: "Nombre", visibility: "public", type: "string" }, + { name: "Stock", visibility: "public", type: "int" }, + ], + constructors: [{ paramCount: 3 }], + methods: [ + { name: "Ajustar", visibility: "public", paramCount: 1 }, + ], + }, + ], + }, + testCases: [ + { + visible: true, + stdin: "P1\nMouse\n5\n3\n", + expectedStdout: "P1 Mouse 8\n", + }, + { + visible: false, + stdin: "X\nCable HDMI\n2\n-3\n", + expectedStdout: "ERROR: Stock insuficiente\n", + }, + { + visible: false, + stdin: "Z\nMonitor\n-1\n0\n", + expectedStdout: "ERROR: Stock invalido\n", + }, + ], + }, + }, + ], +}); diff --git a/prisma/content/courses/csharp-poo-1/units/16-integrador/lessons/02-arquitectura-del-proyecto.ts b/prisma/content/courses/csharp-poo-1/units/16-integrador/lessons/02-arquitectura-del-proyecto.ts new file mode 100644 index 0000000..e01d5bd --- /dev/null +++ b/prisma/content/courses/csharp-poo-1/units/16-integrador/lessons/02-arquitectura-del-proyecto.ts @@ -0,0 +1,137 @@ +import { defineLesson } from "../../../../../authoring"; + +export const leccion02 = defineLesson({ + slug: "arquitectura-del-proyecto", + title: "Arquitectura: dominio, repositorio, XML y protocolo", + description: + "Define dependencias simples entre componentes para poder probar el dominio sin red, archivos ni interfaz gráfica.", + estimatedMinutes: 19, + xpReward: 70, + steps: [ + { + type: "theory", + markdown: `# La integración funciona mejor cuando las piezas ya son independientes + +Una estructura mínima para el proyecto: + +\`\`\`text +UI / Socket + ↓ +ProcesadorComandos + ↓ +Inventario + ↓ +Repositorio + +Inventario ↔ XmlInventario +\`\`\` + +La dirección importa: + +- la UI no accede directamente al diccionario/lista; +- el socket no cambia stock; +- \`ProcesadorComandos\` valida el mensaje y llama al inventario; +- \`Inventario\` protege las reglas; +- \`Repositorio\` resuelve almacenamiento en memoria; +- \`XmlInventario\` serializa/deserializa. + +Esta separación permite que cpp-ceti evalúe casi todo el proyecto con consola. Después el laboratorio local conecta WinForms y sockets a las mismas clases.`, + }, + { + type: "code_example", + code: `using System; +using System.Collections.Generic; +class Entidad{public string Id{get;private set;}public Entidad(string id){Id=id;}} +class Bien:Entidad{public int Stock{get;private set;}public Bien(string id,int stock):base(id){Stock=stock;}public void Ajustar(int c){if(Stock+c<0)throw new ArgumentException("Stock insuficiente");Stock+=c;}} +class Repositorio where T:Entidad +{ + private Dictionary datos=new Dictionary(); + public bool Agregar(T x){if(datos.ContainsKey(x.Id))return false;datos.Add(x.Id,x);return true;} + public T Buscar(string id){T x;return datos.TryGetValue(id,out x)?x:null;} +} +class Inventario +{ + private Repositorio repo=new Repositorio(); + public bool Alta(string id,int stock){return repo.Agregar(new Bien(id,stock));} + public Bien Buscar(string id){return repo.Buscar(id);} +}`, + explanation: + "`Inventario` no expone el diccionario. Un futuro procesador de red sólo llama métodos públicos del servicio.", + runnable: false, + localOnlyNote: + "Fragmento arquitectónico sin Main; la misma separación se ejercita en el reto siguiente.", + }, + { + type: "quiz", + question: + "¿dónde debería vivir la regla “el stock no puede quedar negativo”?", + options: [ + "En `TcpListener`.", + "En el botón de WinForms solamente.", + "En la entidad/servicio de dominio para que todos los adaptadores respeten la misma regla.", + "En el XML como comentario.", + ], + correctIndex: 2, + explanation: + "En la entidad/servicio de dominio para que todos los adaptadores respeten la misma regla.", + }, + { + type: "code_challenge", + exercise: { + prompt: + "Implementa un `Inventario` sobre `Dictionary` privado con métodos `Alta`, `Ajustar`, `Consultar`.\n\nReglas:\n\n- `Alta(codigo,stock)` falla (`false`) si código existe o stock < 0;\n- `Ajustar(codigo,cambio)` falla si no existe o el nuevo stock sería negativo;\n- `Consultar` devuelve `int?` no es necesario: usa `bool Consultar(string codigo, out int stock)`.\n\nProcesa comandos `ALTA codigo stock`, `AJUSTAR codigo cambio`, `BUSCAR codigo`.\n\nRespuestas: `OK`, `ERROR`, o el stock para `BUSCAR`; si no existe, `NO`.", + starterCode: `using System; +using System.Collections.Generic; +class Inventario { } +class Program { static void Main() { } }`, + solutionCode: `using System; +using System.Collections.Generic; +class Inventario +{ + private Dictionary stock=new Dictionary(); + public bool Alta(string c,int s){if(c.Length==0||s<0||stock.ContainsKey(c))return false;stock.Add(c,s);return true;} + public bool Ajustar(string c,int cambio){if(!stock.ContainsKey(c)||stock[c]+cambio<0)return false;stock[c]+=cambio;return true;} + public bool Consultar(string c,out int s){return stock.TryGetValue(c,out s);} +} +class Program +{ + static void Main() + { + int n=int.Parse(Console.ReadLine());Inventario inv=new Inventario(); + for(int i=0;i= 0 y método `Ajustar` que no permita resultado negativo. Lee datos y cambio; imprime stock final o `ERROR`.", + starterCode: `using System; +class Bien { } +class Program { static void Main() { } }`, + solutionCode: `using System; +class Bien{public string Codigo{get;private set;}public string Nombre{get;private set;}public int Stock{get;private set;}public Bien(string c,string n,int s){if(s<0)throw new ArgumentException();Codigo=c;Nombre=n;Stock=s;}public void Ajustar(int x){if(Stock+x<0)throw new ArgumentException();Stock+=x;}} +class Program{static void Main(){try{Bien b=new Bien(Console.ReadLine(),Console.ReadLine(),int.Parse(Console.ReadLine()));b.Ajustar(int.Parse(Console.ReadLine()));Console.WriteLine(b.Stock);}catch(ArgumentException){Console.WriteLine("ERROR");}}}`, + difficulty: "medium", + xpReward: 34, + testCases: [ + { + visible: true, + stdin: "P1\nMouse\n5\n-2\n", + expectedStdout: "3\n", + }, + { + visible: false, + stdin: "X\nCable\n0\n-1\n", + expectedStdout: "ERROR\n", + }, + { + visible: false, + stdin: "Z\nMonitor\n-1\n2\n", + expectedStdout: "ERROR\n", + }, + ], + }, + { + slug: "csharp-poo2-repositorio-dominio", + title: "Repositorio de dominio", + description: + "Encapsula almacenamiento y búsqueda por identidad detrás de un repositorio genérico.", + prompt: + "crea `Entidad(Id)`, `Bien : Entidad`, `Repositorio where T:Entidad` respaldado por `Dictionary` con `Agregar`, `Buscar`, `Eliminar`. Procesa altas/búsquedas/bajas y devuelve `OK`, `DUP`, nombre, `NO`.", + starterCode: `using System; +using System.Collections.Generic; +class Entidad { } +class Bien : Entidad { } +class Repositorio where T : Entidad { } +class Program { static void Main() { } }`, + solutionCode: `using System;using System.Collections.Generic; +class Entidad{public string Id{get;private set;}public Entidad(string id){Id=id;}} +class Bien:Entidad{public string Nombre{get;private set;}public Bien(string id,string n):base(id){Nombre=n;}} +class Repositorio where T:Entidad{private Dictionaryd=new Dictionary();public bool Agregar(T x){if(d.ContainsKey(x.Id))return false;d.Add(x.Id,x);return true;}public T Buscar(string id){T x;return d.TryGetValue(id,out x)?x:null;}public void Eliminar(string id){d.Remove(id);}} +class Program{static void Main(){int n=int.Parse(Console.ReadLine());Repositorior=new Repositorio();for(int i=0;i......`, vuelve a cargarlo y muestra `codigo|nombre|stock`.', + starterCode: `using System; +using System.Xml; +class Program { static void Main() { } }`, + solutionCode: `using System;using System.Xml; +class Program{static void Main(){string c=Console.ReadLine(),n=Console.ReadLine();int s=int.Parse(Console.ReadLine());XmlDocument d=new XmlDocument();XmlElement b=d.CreateElement("bien");b.SetAttribute("codigo",c);XmlElement x=d.CreateElement("nombre");x.InnerText=n;b.AppendChild(x);XmlElement y=d.CreateElement("stock");y.InnerText=s.ToString();b.AppendChild(y);d.AppendChild(b);XmlDocument r=new XmlDocument();r.LoadXml(d.OuterXml);XmlElement z=r.DocumentElement;Console.WriteLine(z.GetAttribute("codigo")+"|"+z["nombre"].InnerText+"|"+z["stock"].InnerText);}}`, + difficulty: "medium", + xpReward: 34, + testCases: [ + { + visible: true, + stdin: "P1\nMouse\n5\n", + expectedStdout: "P1|Mouse|5\n", + }, + { + visible: false, + stdin: "X\nA & B\n0\n", + expectedStdout: "X|A & B|0\n", + }, + { + visible: false, + stdin: "Z\nCable HDMI\n17\n", + expectedStdout: "Z|Cable HDMI|17\n", + }, + ], + }, + { + slug: "csharp-poo2-comandos-thread-safe", + title: "Comandos thread-safe", + description: + "Aplica operaciones concurrentes a un servicio que protege internamente su estado.", + prompt: + "crea `Inventario` con stock privado, `Ajustar(int)` y `Consultar()`, ambos bajo el mismo lock. Lee `n`; crea dos hilos, uno suma `1` n veces y otro suma `2` n veces. Tras ambos `Join`, imprime `3*n`.", + starterCode: `using System; +using System.Threading; +class Inventario { } +class Program { static void Main() { } }`, + solutionCode: `using System;using System.Threading; +class Inventario{private int stock;private readonly object g=new object();public void Ajustar(int x){lock(g){stock+=x;}}public int Consultar(){lock(g){return stock;}}} +class Program{static int n;static Inventario inv=new Inventario();static void A(){for(int i=0;i Date: Wed, 2 Sep 2026 06:02:11 +0000 Subject: [PATCH 2/2] Actualiza contrato de tests de contenido para el nuevo baseline S3+S4 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit tests/content/course-contract.test.ts: reemplaza el describe de "curriculum de C#: 1 sección" por el contrato de las 2 secciones (S3 sin cambios académicos: 8 units/33 lessons/161 steps/32 practices; S4: 8 units/40 lessons/32 practices, todas unpublished); fija el baseline total (16 units, 73 lessons, 321 steps, 64 practices); agrega un describe de política de ejecución de S4 (ningún ejemplo/práctica de redes abre socket real, ningún reto de concurrencia con Thread calificado por stdout sin Join/lock, localOnlyNote presente en todo code_example no ejecutable, las 8 unidades S4 unpublished). tests/content/authoring.test.ts: el describe de equivalencia legacy para C# ahora compara sólo la porción S3 (unidades/practicas legacy) contra `cursoCsharpPoo1`/`csharpPracticeSets`, en vez del Course completo — S4 es contenido nuevo sin contraparte legacy, y la metadata general del Course cambió a propósito para cubrir ambas asignaturas. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01SopFLn5n6iqCyq7zGQN4JS --- tests/content/authoring.test.ts | 36 +++-- tests/content/course-contract.test.ts | 200 ++++++++++++++++++++++++-- 2 files changed, 216 insertions(+), 20 deletions(-) diff --git a/tests/content/authoring.test.ts b/tests/content/authoring.test.ts index ac25d04..be3e3fc 100644 --- a/tests/content/authoring.test.ts +++ b/tests/content/authoring.test.ts @@ -933,23 +933,27 @@ describe("equivalencia legacy: el refactor de authoring no cambia contenido", () expect(cpp.units).toEqual(legacyCursoCpp.units); }); - it("C#: metadata de curso idéntica", () => { + it("C#: identidad de curso preservada (slug/lenguaje/perfil) tras agregar S4", () => { + // El Course ahora cubre POO I + POO II: title/description/subjectName + // cambiaron a propósito (ver "la metadata general del Course cubre + // ambas asignaturas" en course-contract.test.ts) y ya NO deben ser + // idénticos al legacy. Lo que este test protege es que `adaptLegacyUnits` + // no tocó la identidad que S4 no tiene permitido cambiar. const csharp = allCourses.find((c) => c.slug === "csharp-poo-1")!; expect(csharp.slug).toBe(legacyCursoCsharp.slug); - expect(csharp.title).toBe(legacyCursoCsharp.title); - expect(csharp.description).toBe(legacyCursoCsharp.description); - expect(csharp.subjectName).toBe(legacyCursoCsharp.subjectName); - expect(csharp.academicContext).toBe(legacyCursoCsharp.academicContext); expect(csharp.language).toBe(legacyCursoCsharp.language); expect(csharp.executionProfile).toBe(legacyCursoCsharp.executionProfile); }); - it("C#: mismo orden de unidades, unidades, lecciones y steps idénticos", () => { + it("C#: las 8 unidades legacy de S3 siguen idénticas y en el mismo orden dentro del Course", () => { + // El Course ahora tiene 16 unidades (S3 + S4 nueva); sólo las primeras + // 8 son el contenido legacy que este test de equivalencia protege. const csharp = allCourses.find((c) => c.slug === "csharp-poo-1")!; - expect(csharp.units.map((u) => u.slug)).toEqual( + const s3Units = csharp.units.slice(0, legacyCursoCsharp.units.length); + expect(s3Units.map((u) => u.slug)).toEqual( legacyCursoCsharp.units.map((u) => u.slug), ); - expect(csharp.units).toEqual(legacyCursoCsharp.units); + expect(s3Units).toEqual(legacyCursoCsharp.units); }); it("C++: mismos grupos de práctica (por unidad), exercises y test cases idénticos y en el mismo orden dentro de cada grupo", () => { @@ -974,9 +978,14 @@ describe("equivalencia legacy: el refactor de authoring no cambia contenido", () } }); - it("C#: mismos grupos de práctica (por unidad), exercises y test cases idénticos y en el mismo orden dentro de cada grupo", () => { + it("C#: mismos grupos de práctica de S3 (por unidad), exercises y test cases idénticos y en el mismo orden dentro de cada grupo", () => { + // S4 agregó 8 grupos de práctica NUEVOS que no existen en el legacy: + // este test de equivalencia sólo protege los 8 grupos de S3. + const legacyUnitSlugs = new Set(legacyCursoCsharp.units.map((u) => u.slug)); const canonical = byUnitSlug( - allPracticeSets.filter((s) => s.courseSlug === "csharp-poo-1"), + allPracticeSets.filter( + (s) => s.courseSlug === "csharp-poo-1" && legacyUnitSlugs.has(s.unitSlug), + ), ); const legacy = byUnitSlug(legacyCsharpPracticeSets); @@ -985,7 +994,7 @@ describe("equivalencia legacy: el refactor de authoring no cambia contenido", () // unidades del curso: lo comprobamos también posicionalmente. expect( allPracticeSets - .filter((s) => s.courseSlug === "csharp-poo-1") + .filter((s) => s.courseSlug === "csharp-poo-1" && legacyUnitSlugs.has(s.unitSlug)) .map((s) => s.unitSlug), ).toEqual(legacyCsharpPracticeSets.map((s) => s.unitSlug)); @@ -1019,8 +1028,11 @@ describe("equivalencia legacy: el refactor de authoring no cambia contenido", () } } + // Sólo las primeras 8 unidades del Course son el legacy S3 que este + // test de equivalencia protege; S4 (unidades 9-16) es contenido nuevo, + // sin contraparte legacy con la que comparar. const csharp = allCourses.find((c) => c.slug === "csharp-poo-1")!; - for (let i = 0; i < csharp.units.length; i++) { + for (let i = 0; i < legacyCursoCsharp.units.length; i++) { expect(csharp.units[i].published).toBe(legacyCursoCsharp.units[i].published); } }); diff --git a/tests/content/course-contract.test.ts b/tests/content/course-contract.test.ts index 72177cc..cb4ff83 100644 --- a/tests/content/course-contract.test.ts +++ b/tests/content/course-contract.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it } from "vitest"; import { allCourses } from "../../prisma/content"; import { allPracticeSets } from "../../prisma/content/exercises"; -import type { StepDefinition } from "../../prisma/content/types"; +import type { CodeExampleStep, StepDefinition } from "../../prisma/content/types"; import { isCompatible, LANGUAGE_PROFILES } from "@/lib/code-languages"; /** @@ -294,19 +294,203 @@ describe("curriculum de C++: 2 secciones semestrales", () => { }); }); -describe("curriculum de C#: 1 sección (demuestra que no está hardcodeado a C++)", () => { +describe("curriculum de C#: 2 secciones (S3 POO I + S4 POO II)", () => { const csharp = courseBySlug.get("csharp-poo-1")!; - it("declara exactamente 1 section: semester 3, subjectName exacto, 8 Units existentes", () => { + const unitBySlug = new Map(csharp.units.map((u) => [u.slug, u])); + const lessonsOf = (slugs: string[]) => + slugs.reduce((n, slug) => n + unitBySlug.get(slug)!.lessons.length, 0); + const stepsOf = (slugs: string[]) => + slugs.reduce( + (n, slug) => + n + unitBySlug.get(slug)!.lessons.reduce((m, l) => m + l.steps.length, 0), + 0, + ); + const practicesOf = (slugs: string[]) => + allPracticeSets + .filter((s) => s.courseSlug === "csharp-poo-1" && slugs.includes(s.unitSlug)) + .reduce((n, s) => n + s.exercises.length, 0); + + it("declara exactamente 2 sections, en orden: S3 (semester 3) y S4 (semester 4)", () => { expect(csharp.curriculum).toBeDefined(); - expect(csharp.curriculum).toHaveLength(1); + expect(csharp.curriculum).toHaveLength(2); + expect(csharp.curriculum!.map((s) => s.key)).toEqual([ + "s3-programacion-orientada-objetos-1", + "s4-programacion-orientada-objetos-2", + ]); + }); - const [s3] = csharp.curriculum!; - expect(s3.key).toBe("s3-programacion-orientada-objetos-1"); + it("S3: semester 3, subjectName exacto, 8 unidades EXISTENTES, mismo orden, sin cambios académicos", () => { + const s3 = csharp.curriculum!.find( + (s) => s.key === "s3-programacion-orientada-objetos-1", + )!; expect(s3.semester).toBe(3); expect(s3.order).toBe(1); expect(s3.subjectName).toBe("Programación Orientada a Objetos I"); - expect(s3.unitSlugs).toEqual(csharp.units.map((u) => u.slug)); - expect(s3.unitSlugs).toHaveLength(8); + expect(s3.unitSlugs).toEqual([ + "csharp-poo-01-modelar", + "csharp-poo-02-encapsular", + "csharp-poo-03-uml", + "csharp-poo-04-relaciones", + "csharp-poo-05-herencia", + "csharp-poo-06-diseno-robusto", + "csharp-poo-07-gui", + "csharp-poo-08-integrador", + ]); + expect(lessonsOf(s3.unitSlugs)).toBe(33); + expect(stepsOf(s3.unitSlugs)).toBe(161); + expect(practicesOf(s3.unitSlugs)).toBe(32); + // GUI e integrador siguen despublicadas hasta la aceptación en Windows; + // el resto de S3 sigue publicado, exactamente como antes de S4. + expect(s3.unitSlugs.map((slug) => unitBySlug.get(slug)!.published)).toEqual([ + true, + true, + true, + true, + true, + true, + false, + false, + ]); + }); + + it("S4: semester 4, subjectName exacto, 8 unidades EXACTAS en el orden contractual, todas unpublished", () => { + const s4 = csharp.curriculum!.find( + (s) => s.key === "s4-programacion-orientada-objetos-2", + )!; + expect(s4.semester).toBe(4); + expect(s4.order).toBe(2); + expect(s4.subjectName).toBe("Programación Orientada a Objetos II"); + expect(s4.unitSlugs).toEqual([ + "csharp-poo2-01-colecciones", + "csharp-poo2-02-diccionarios", + "csharp-poo2-03-ordenamiento", + "csharp-poo2-04-xml", + "csharp-poo2-05-genericos", + "csharp-poo2-06-concurrencia", + "csharp-poo2-07-redes", + "csharp-poo2-08-integrador", + ]); + expect(lessonsOf(s4.unitSlugs)).toBe(40); + expect(practicesOf(s4.unitSlugs)).toBe(32); + for (const slug of s4.unitSlugs) { + expect(unitBySlug.get(slug)!.published, slug).toBe(false); + } + }); + + it("el orden GLOBAL aplanado (Unit.order) pone S4 completo después de S3 completo", () => { + expect(csharp.units.map((u) => u.slug)).toEqual([ + "csharp-poo-01-modelar", + "csharp-poo-02-encapsular", + "csharp-poo-03-uml", + "csharp-poo-04-relaciones", + "csharp-poo-05-herencia", + "csharp-poo-06-diseno-robusto", + "csharp-poo-07-gui", + "csharp-poo-08-integrador", + "csharp-poo2-01-colecciones", + "csharp-poo2-02-diccionarios", + "csharp-poo2-03-ordenamiento", + "csharp-poo2-04-xml", + "csharp-poo2-05-genericos", + "csharp-poo2-06-concurrencia", + "csharp-poo2-07-redes", + "csharp-poo2-08-integrador", + ]); + }); + + it("totales del curso: 16 units, 73 lessons, 321 steps, 64 practices", () => { + const lessons = csharp.units.flatMap((u) => u.lessons); + const steps = lessons.flatMap((l) => l.steps); + const total = allPracticeSets + .filter((s) => s.courseSlug === "csharp-poo-1") + .reduce((n, s) => n + s.exercises.length, 0); + + expect(csharp.units.length).toBe(16); + expect(lessons.length).toBe(73); + expect(steps.length).toBe(321); + expect(total).toBe(64); + }); + + it("la metadata general del Course cubre ambas asignaturas", () => { + expect(csharp.title).toBe("Programación Orientada a Objetos con C#"); + expect(csharp.subjectName).toBe("Programación Orientada a Objetos"); + expect(csharp.slug).toBe("csharp-poo-1"); + expect(csharp.language).toBe("csharp"); + expect(csharp.executionProfile).toBe("csharp-mono-6.12"); + }); +}); + +describe("política de ejecución de S4 (red, sockets, concurrencia no determinista)", () => { + const csharp = courseBySlug.get("csharp-poo-1")!; + const s4 = csharp.curriculum!.find( + (s) => s.key === "s4-programacion-orientada-objetos-2", + )!; + const s4Units = csharp.units.filter((u) => s4.unitSlugs.includes(u.slug)); + + function codeExampleSteps() { + return s4Units.flatMap((u) => + u.lessons.flatMap((l) => + l.steps + .filter((s): s is CodeExampleStep => s.type === "code_example") + .map((step) => ({ where: `${u.slug}/${l.slug}`, step })), + ), + ); + } + + it("ningún ejemplo/reto ejecutable de la unidad de redes abre un socket real", () => { + const SOCKET_MARKERS = /\b(TcpListener|TcpClient|UdpClient|Socket\s*\(|\.Accept\(|\.Connect\(|\.Receive\(|\.Send\()/; + for (const { where, step } of codeExampleSteps()) { + if (!where.startsWith("csharp-poo2-07-redes/")) continue; + if (!SOCKET_MARKERS.test(step.code)) continue; + expect(step.runnable === true, `${where}: abre socket real y es ejecutable`).toBe( + false, + ); + expect(step.localOnlyNote?.trim(), `${where}: falta localOnlyNote`).toBeTruthy(); + } + }); + + it("ninguna práctica de redes intenta abrir un socket real", () => { + const redesSet = allPracticeSets.find( + (s) => s.courseSlug === "csharp-poo-1" && s.unitSlug === "csharp-poo2-07-redes", + )!; + expect(redesSet).toBeDefined(); + const SOCKET_MARKERS = /\b(TcpListener|TcpClient|UdpClient|Socket\s*\()/; + for (const ex of redesSet.exercises) { + expect( + SOCKET_MARKERS.test(ex.solutionCode), + `${ex.slug}: la solución calificada abre un socket real`, + ).toBe(false); + } + }); + + it("la unidad de concurrencia no califica por stdout una demostración de race condition", () => { + const concurrencia = s4Units.find((u) => u.slug === "csharp-poo2-06-concurrencia")!; + for (const lesson of concurrencia.lessons) { + for (const step of lesson.steps) { + if (step.type !== "code_challenge") continue; + // Un reto calificado de concurrencia debe sincronizar (Join/lock) para + // ser determinista; no debe depender de una carrera sin protección. + const usesThread = /\bThread\b/.test(step.exercise.solutionCode); + if (!usesThread) continue; + expect( + /\bJoin\s*\(|\block\s*\(/.test(step.exercise.solutionCode), + `${lesson.slug}: reto con Thread calificado por stdout sin Join/lock`, + ).toBe(true); + } + } + }); + + it("todo code_example de S4 marcado no ejecutable trae localOnlyNote", () => { + for (const { where, step } of codeExampleSteps()) { + if (step.runnable === true) continue; + expect(step.localOnlyNote?.trim(), `${where}: falta localOnlyNote`).toBeTruthy(); + } + }); + + it("las 8 unidades de S4 están unpublished (release gate pendiente)", () => { + for (const unit of s4Units) { + expect(unit.published, unit.slug).toBe(false); + } }); });