SELECT en SQL: consultas, WHERE, ORDER BY, LIMIT y DISTINCT con ejemplos

SELECT es la sentencia más utilizada para leer información de una base de datos. Antes de combinar consultas complejas, conviene entender bien qué hace cada parte.

¿Qué problema resuelve SELECT?

Una tabla puede contener miles o millones de registros. SELECT nos permite pedir exactamente los datos que necesitamos.

Cómo pensar una consulta SELECT

Antes de escribir SQL conviene hacerse cuatro preguntas:

  1. ¿de qué tabla salen los datos?
  2. ¿qué columnas necesito?
  3. ¿qué filas quiero conservar?
  4. ¿cómo quiero ordenar o limitar el resultado?

Con ese razonamiento, la consulta deja de ser una lista de palabras reservadas y pasa a representar una pregunta concreta a la base de datos.

La estructura básica

SELECT columnas
FROM tabla;

SELECT indica qué columnas queremos obtener y FROM de qué tabla provienen.

SELECT *

SELECT *
FROM alumnos;

El asterisco significa “todas las columnas”. Es cómodo para aprender, pero en sistemas reales conviene seleccionar solo lo necesario.

Seleccionar columnas concretas

SELECT nombre, apellido
FROM alumnos;

El resultado contiene únicamente esas dos columnas.

WHERE: filtrar filas

WHERE define una condición. Solo se devuelven las filas que la cumplen.

SELECT *
FROM alumnos
WHERE edad = 25;

Operadores de comparación

  • = igual
  • <> o != distinto
  • > mayor
  • < menor
  • >= mayor o igual
  • <= menor o igual

Comparar texto

SELECT *
FROM alumnos
WHERE ciudad = 'Santa Rosa';

Los valores de texto se escriben entre comillas.

NULL: ausencia de valor

NULL significa que no hay un valor conocido. No equivale a cero ni a una cadena vacía.

Por eso no se compara con = NULL. Debemos usar:

SELECT *
FROM alumnos
WHERE email IS NULL;

Y para buscar registros con valor:

WHERE email IS NOT NULL

AND

AND exige que se cumplan todas las condiciones.

SELECT *
FROM alumnos
WHERE ciudad = 'Santa Rosa'
AND edad >= 22;

OR

OR acepta una condición u otra.

SELECT *
FROM alumnos
WHERE ciudad = 'Santa Rosa'
OR ciudad = 'General Pico';

AND y OR juntos: usar paréntesis

Cuando combinamos AND y OR, los paréntesis hacen explícita la lógica y evitan resultados inesperados.

SELECT *
FROM alumnos
WHERE ciudad = 'Santa Rosa'
AND (edad < 20 OR edad > 30);

Primero se evalúa lo que está entre paréntesis y luego se combina con la condición de ciudad.

NOT

NOT niega una condición.

SELECT *
FROM alumnos
WHERE NOT ciudad = 'Santa Rosa';

BETWEEN

Sirve para trabajar con rangos.

SELECT *
FROM alumnos
WHERE edad BETWEEN 22 AND 26;

IN

Evita escribir muchos OR cuando comparamos contra varios valores.

SELECT *
FROM alumnos
WHERE ciudad IN ('Santa Rosa', 'General Pico', 'Toay');

LIKE

LIKE busca patrones.

SELECT *
FROM alumnos
WHERE nombre LIKE 'A%';

% representa cero o más caracteres.

También existe _, que representa exactamente un carácter.

SELECT *
FROM alumnos
WHERE nombre LIKE 'A_a';

Este patrón podría coincidir con nombres como Ana, porque hay exactamente un carácter entre la A y la a.

ORDER BY

Ordena el resultado.

SELECT *
FROM alumnos
ORDER BY edad ASC;

ASC ordena ascendente y DESC descendente.

Ordenar por varias columnas

SELECT *
FROM alumnos
ORDER BY ciudad ASC, apellido ASC;

AS: poner nombres más claros al resultado

Un alias cambia el nombre mostrado de una columna sin modificar la tabla.

SELECT nombre AS alumno,
       email AS correo
FROM alumnos;

LIMIT

Limita cuántas filas devuelve la consulta.

SELECT *
FROM alumnos
LIMIT 3;

OFFSET

Permite saltar registros. Es habitual en paginación.

Por ejemplo, con 10 registros por página, la segunda página podría usar LIMIT 10 OFFSET 10. En tablas muy grandes, offsets muy altos pueden volverse costosos y existen técnicas de paginación por clave más eficientes.

SELECT *
FROM alumnos
ORDER BY id
LIMIT 2 OFFSET 2;

DISTINCT

Elimina valores repetidos del resultado.

SELECT DISTINCT ciudad
FROM alumnos;

Ejemplo combinado

SELECT nombre, apellido, edad
FROM alumnos
WHERE ciudad = 'Santa Rosa'
AND edad >= 20
ORDER BY edad DESC
LIMIT 5;

Conceptualmente, esta consulta:

  1. parte de alumnos;
  2. conserva los de Santa Rosa;
  3. descarta menores de 20;
  4. ordena por edad de mayor a menor;
  5. devuelve solo cinco filas.

Ese orden mental es muy útil para construir consultas más grandes.

Errores comunes

  • olvidar comillas en textos;
  • confundir AND y OR;
  • usar LIKE sin %;
  • suponer que una tabla tiene orden natural;
  • usar SELECT * cuando solo hacen falta dos columnas.

Ejercicios

  1. mostrar todos los alumnos;
  2. mostrar nombre y email;
  3. buscar mayores de 23;
  4. buscar alumnos de Santa Rosa;
  5. ordenar por edad;
  6. mostrar los tres mayores;
  7. listar ciudades sin repetir.

¿Qué sigue?

Funciones de agregación para resumir información.

¿Te sirvió esta guía? ☕

Si este contenido te ayudó y querés apoyar a Club Programador para seguir publicando ejercicios, proyectos y guías gratuitas, podés colaborar mediante:

☕ Apoyar con Mercado Pago

🌎 Apoyar con PayPal


Ruta SQL / MySQL

Ver todos los contenidos de SQL / MySQL

← Introducción a SQL y MySQL

Funciones de agregación →

Introducción a SQL y MySQL: bases de datos, tablas, registros y consultas desde cero

Cuando una aplicación necesita guardar información de forma permanente, ordenada y consultable, normalmente termina utilizando una base de datos. Un sistema de alumnos, una tienda, una red social, un banco o una plataforma médica tienen algo en común: necesitan almacenar datos y recuperarlos de forma confiable.

En esta primera guía vamos a construir una base conceptual sólida antes de empezar con las consultas. La idea es entender qué problema resuelven las bases de datos, qué es SQL, qué es MySQL y cómo se organiza la información.

¿Qué problema resuelve una base de datos?

Imaginemos una escuela que guarda sus alumnos en archivos de texto separados. Cada archivo podría contener nombre, documento, curso y teléfono. Al principio puede funcionar, pero enseguida aparecen problemas:

  • ¿cómo buscamos rápidamente a todos los alumnos de un curso?
  • ¿cómo evitamos cargar dos veces al mismo alumno?
  • ¿cómo actualizamos un teléfono sin modificar varios archivos?
  • ¿cómo relacionamos alumnos con cursos, profesores y materias?
  • ¿cómo permitimos que varias personas trabajen al mismo tiempo sin romper la información?

Una base de datos está diseñada precisamente para resolver ese tipo de problemas.

Dato, información y estructura

Un dato es un valor individual: por ejemplo 22, Ana o Santa Rosa. Cuando varios datos se organizan y adquieren contexto, se transforman en información útil.

Por ejemplo:

nombre: Ana
edad: 22
ciudad: Santa Rosa

En una base de datos, esos valores no se guardan de manera arbitraria: se organizan dentro de estructuras definidas.

¿Qué es una base de datos?

Una base de datos es un conjunto organizado de información que puede almacenarse, consultarse, modificarse y relacionarse de forma eficiente.

No es simplemente un archivo con datos. Una base de datos permite aplicar reglas, relaciones, permisos, restricciones y transacciones.

¿Cómo funciona una base de datos en una aplicación?

En una aplicación típica, el usuario no habla directamente con MySQL. La aplicación recibe una acción, construye una consulta SQL, la envía al servidor de base de datos y recibe un resultado.

Podemos imaginar el flujo así:

Usuario → Aplicación → SQL → MySQL → Resultado → Aplicación → Usuario

Esta separación es importante porque el motor de base de datos se especializa en almacenar, validar, buscar y relacionar información, mientras que la aplicación se ocupa de la interfaz y las reglas del sistema.

¿Qué es un SGBD?

Un Sistema Gestor de Bases de Datos o SGBD es el software encargado de administrar una base de datos.

Entre otras tareas, se ocupa de:

  • guardar y recuperar información;
  • procesar consultas;
  • controlar accesos;
  • mantener integridad;
  • gestionar concurrencia;
  • manejar transacciones;
  • optimizar consultas.

MySQL es un SGBD relacional.

¿Qué es SQL?

SQL significa Structured Query Language. Es el lenguaje que usamos para comunicarnos con bases de datos relacionales.

Con SQL podemos:

  • crear tablas;
  • insertar datos;
  • consultar información;
  • modificar registros;
  • eliminar datos;
  • crear relaciones;
  • administrar permisos;
  • controlar transacciones.

SQL es un lenguaje declarativo

Esto significa que normalmente indicamos qué resultado queremos, y el motor decide cómo obtenerlo.

Por ejemplo:

SELECT nombre, apellido
FROM alumnos
WHERE edad > 18;

Nosotros pedimos “los alumnos mayores de 18”. MySQL decide cómo localizar esos registros.

SQL y MySQL no son lo mismo

SQL es el lenguaje. MySQL es uno de los motores que implementan ese lenguaje.

También existen otros motores como PostgreSQL, SQL Server, Oracle o MariaDB. Todos utilizan SQL, aunque cada uno posee extensiones y diferencias propias.

Base de datos, esquema y tabla: no son lo mismo

Conviene distinguir tres conceptos:

  • base de datos: el conjunto general de información;
  • esquema: la organización lógica de objetos y estructuras;
  • tabla: una estructura concreta formada por columnas y filas.

En MySQL, en el uso cotidiano, los términos base de datos y schema suelen tratarse prácticamente como equivalentes.

¿Qué es una base de datos relacional?

Una base de datos relacional organiza la información en tablas. Esas tablas pueden relacionarse mediante claves.

Por ejemplo, en un sistema educativo podríamos tener:

  • alumnos;
  • cursos;
  • profesores;
  • inscripciones.

En lugar de guardar toda la información repetida en una única tabla enorme, se divide de forma lógica y después se relaciona.

Entidad, atributo y registro

Una entidad representa algo del mundo real que queremos guardar. Por ejemplo, un alumno.

Sus atributos podrían ser nombre, apellido, fecha de nacimiento o email.

Un registro es una instancia concreta de esa entidad.

Tabla, fila y columna

En una tabla:

  • cada fila representa un registro;
  • cada columna representa un atributo;
  • la tabla completa representa una entidad o relación.

Clave primaria

Una clave primaria identifica de forma única cada fila.

id INT AUTO_INCREMENT PRIMARY KEY

Dos registros no pueden compartir el mismo valor de clave primaria.

Clave foránea

Una clave foránea permite relacionar una tabla con otra.

Por ejemplo, un alumno puede guardar un curso_id que apunte al id de la tabla cursos.

¿Por qué relacionar tablas en lugar de repetir datos?

Supongamos que 500 alumnos pertenecen al mismo curso. Si escribimos el nombre completo del curso en cada alumno, repetimos el mismo dato 500 veces.

En cambio, podemos guardar el curso una sola vez:

cursos
id | nombre
1  | Programación

alumnos
id | nombre | curso_id
1  | Ana    | 1
2  | Juan   | 1

El valor curso_id = 1 significa que ambos alumnos apuntan al mismo curso. Si el nombre del curso cambia, se modifica una sola fila.

Relaciones entre tablas

Las relaciones principales son:

  • uno a uno: un registro se relaciona con uno;
  • uno a muchos: un registro se relaciona con muchos;
  • muchos a muchos: varios registros de una tabla se relacionan con varios de otra.

Concurrencia e integridad

Una de las grandes diferencias frente a guardar información en archivos sueltos es que un SGBD está preparado para que varios usuarios trabajen al mismo tiempo.

Además, puede hacer cumplir reglas. Por ejemplo, puede impedir que dos usuarios tengan el mismo email si definimos una restricción UNIQUE, o evitar que un alumno apunte a un curso inexistente mediante una clave foránea.

Persistencia y transacciones

Los datos sobreviven al cierre de la aplicación porque se almacenan de forma persistente. Y cuando una operación requiere varios pasos, las transacciones permiten tratarlos como una sola unidad: o se completan todos o se deshacen.

Ventajas de una base de datos relacional

  • organización clara;
  • menos duplicación innecesaria;
  • integridad mediante claves y restricciones;
  • consultas potentes;
  • seguridad;
  • transacciones;
  • mantenimiento más sencillo;
  • capacidad para manejar grandes volúmenes de información.

Tipos de instrucciones SQL

SQL suele dividirse conceptualmente en grupos:

  • DDL: define estructuras. Ejemplo: CREATE TABLE.
  • DML: modifica datos. Ejemplo: INSERT, UPDATE, DELETE.
  • DQL: consulta datos. Principalmente SELECT.
  • TCL: controla transacciones. Ejemplo: COMMIT, ROLLBACK.
  • DCL: administra permisos y accesos.

Crear nuestra primera base

CREATE DATABASE colegio;
USE colegio;

Crear una tabla

CREATE TABLE alumnos (
    id INT AUTO_INCREMENT PRIMARY KEY,
    nombre VARCHAR(100),
    apellido VARCHAR(100),
    edad INT,
    email VARCHAR(150)
);

Insertar un registro

INSERT INTO alumnos (nombre, apellido, edad, email)
VALUES ('Ana', 'Pérez', 22, 'ana@email.com');

Consultar datos

SELECT * FROM alumnos;

El asterisco indica que queremos todas las columnas.

Modificar datos

UPDATE alumnos
SET edad = 23
WHERE id = 1;

Eliminar datos

DELETE FROM alumnos
WHERE id = 1;

CRUD

Estas cuatro operaciones fundamentales se conocen como CRUD:

  • Create: crear;
  • Read: leer;
  • Update: modificar;
  • Delete: eliminar.

Ejercicio práctico

Creá una base llamada biblioteca y una tabla libros con:

  • id;
  • título;
  • autor;
  • año;
  • disponible.

¿Qué sigue?

En la próxima guía vamos a aprender a consultar datos con SELECT, aplicar filtros con WHERE y ordenar resultados.

¿Te sirvió esta guía? ☕

Si este contenido te ayudó y querés apoyar a Club Programador para seguir publicando ejercicios, proyectos y guías gratuitas, podés colaborar mediante:

☕ Apoyar con Mercado Pago

🌎 Apoyar con PayPal


Ruta SQL / MySQL

Ver todos los contenidos de SQL / MySQL

SELECT en SQL →

Librerías básicas de Python que todo principiante debería conocer

Cuando empezamos a programar en Python, una de las grandes ventajas del lenguaje es la enorme cantidad de herramientas que ya vienen incluidas. Estas herramientas se agrupan en módulos y librerías que podemos importar para resolver tareas comunes sin tener que programarlo todo desde cero.

En esta guía vamos a conocer algunas de las librerías básicas de Python que todo principiante debería manejar, con ejemplos simples y prácticos.

¿Qué es una librería en Python?

Una librería es un conjunto de funcionalidades reutilizables que podemos incorporar a nuestros programas.

Por ejemplo, si necesitamos calcular una raíz cuadrada, no hace falta escribir el algoritmo manualmente. Podemos usar el módulo math:

import math

numero = 25
resultado = math.sqrt(numero)

print(resultado)

Resultado:

5.0

Biblioteca estándar vs paquetes externos

Python incluye una gran cantidad de módulos en su biblioteca estándar. Estos no necesitan instalación adicional.

Por ejemplo:

import math
import random
import json

En cambio, existen paquetes externos como pygame, requests o pandas que normalmente deben instalarse con pip.

pip install pygame

La diferencia es importante: importar no es lo mismo que instalar.

1. math

El módulo math permite realizar operaciones matemáticas comunes.

import math

print(math.sqrt(81))
print(math.pow(2, 5))
print(math.pi)
print(math.ceil(4.2))
print(math.floor(4.8))

Algunas funciones útiles:

  • sqrt(): raíz cuadrada;
  • pow(): potencia;
  • ceil(): redondeo hacia arriba;
  • floor(): redondeo hacia abajo;
  • pi: constante π.

2. random

random se utiliza para trabajar con valores aleatorios.

import random

numero = random.randint(1, 10)

print(numero)

También podemos elegir un elemento al azar:

import random

nombres = ["Ana", "Juan", "Pedro", "Lucía"]

ganador = random.choice(nombres)

print(ganador)

Es especialmente útil para juegos, sorteos, simulaciones y generación de datos de prueba.

3. datetime

El módulo datetime permite trabajar con fechas y horas.

from datetime import datetime

ahora = datetime.now()

print(ahora)

También podemos acceder a partes individuales:

print(ahora.year)
print(ahora.month)
print(ahora.day)
print(ahora.hour)

Y formatear la fecha:

fecha = ahora.strftime("%d/%m/%Y")

print(fecha)

4. os

os permite interactuar con el sistema operativo.

import os

print(os.getcwd())

getcwd() devuelve la carpeta actual desde donde se está ejecutando el programa.

También podemos listar archivos:

archivos = os.listdir(".")

for archivo in archivos:
    print(archivo)

Crear una carpeta:

os.mkdir("nueva_carpeta")

5. sys

El módulo sys ofrece información y herramientas relacionadas con el intérprete de Python.

import sys

print(sys.version)

También permite leer argumentos enviados desde la terminal.

Si ejecutamos:

python programa.py Jorge

Podemos acceder al valor con:

import sys

print(sys.argv)

6. statistics

statistics simplifica cálculos estadísticos básicos.

import statistics

numeros = [10, 20, 30, 40, 50]

print(statistics.mean(numeros))
print(statistics.median(numeros))

Funciones frecuentes:

  • mean(): promedio;
  • median(): mediana;
  • mode(): moda.

7. json

JSON es uno de los formatos más utilizados para intercambiar y guardar datos.

Python incluye el módulo json para trabajar con este formato.

import json

persona = {
    "nombre": "Ana",
    "edad": 25
}

texto_json = json.dumps(persona)

print(texto_json)

También podemos guardar datos en un archivo:

import json

persona = {
    "nombre": "Ana",
    "edad": 25
}

with open("persona.json", "w", encoding="utf-8") as archivo:
    json.dump(persona, archivo, ensure_ascii=False, indent=4)

8. csv

Los archivos CSV se utilizan mucho para exportar e importar información en hojas de cálculo y sistemas administrativos.

import csv

datos = [
    ["Nombre", "Edad"],
    ["Ana", 25],
    ["Pedro", 32]
]

with open("personas.csv", "w", newline="", encoding="utf-8") as archivo:
    escritor = csv.writer(archivo)
    escritor.writerows(datos)

9. pathlib

pathlib ofrece una forma moderna y clara de trabajar con rutas y archivos.

from pathlib import Path

ruta = Path("datos")

if not ruta.exists():
    ruta.mkdir()

También podemos crear rutas de manera sencilla:

archivo = ruta / "usuarios.txt"

print(archivo)

Para código nuevo, muchas veces pathlib resulta más cómodo que trabajar directamente con cadenas y os.path.

10. time

time permite trabajar con pausas y mediciones simples de tiempo.

import time

print("Comenzando...")

time.sleep(2)

print("Pasaron dos segundos")

También podemos medir cuánto tarda una parte del programa:

import time

inicio = time.time()

for i in range(1000000):
    resultado = i * 2

fin = time.time()

print("Tiempo:", fin - inicio)

Formas de importar módulos

Podemos importar un módulo completo:

import math

print(math.sqrt(16))

O importar solamente lo que necesitamos:

from math import sqrt

print(sqrt(16))

También podemos usar un alias:

import statistics as stats

numeros = [10, 20, 30]

print(stats.mean(numeros))

Módulo, paquete y librería: ¿son lo mismo?

En conversaciones cotidianas se suelen usar como sinónimos, pero técnicamente existen diferencias:

  • módulo: normalmente es un archivo Python que contiene funciones, clases o variables;
  • paquete: agrupa varios módulos;
  • librería: término general para un conjunto de herramientas reutilizables.

Para empezar no es necesario memorizar estas diferencias, pero conviene saber que existen.

Ejercicio práctico: generar estadísticas y guardarlas en JSON

Vamos a combinar random, statistics y json.

import random
import statistics
import json

numeros = []

for i in range(20):
    numeros.append(random.randint(1, 100))

resultado = {
    "numeros": numeros,
    "promedio": statistics.mean(numeros),
    "mediana": statistics.median(numeros),
    "minimo": min(numeros),
    "maximo": max(numeros)
}

print(resultado)

with open("estadisticas.json", "w", encoding="utf-8") as archivo:
    json.dump(resultado, archivo, ensure_ascii=False, indent=4)

Este pequeño programa:

  • genera 20 números aleatorios;
  • calcula promedio y mediana;
  • obtiene mínimo y máximo;
  • guarda toda la información en un archivo JSON.

Desafío

Modificá el ejercicio anterior para que también guarde la fecha y hora en que se generaron las estadísticas utilizando datetime.

Después generá un archivo CSV con todos los números utilizando el módulo csv.

¿Qué librería conviene aprender primero?

No hace falta estudiar todas de memoria. Lo importante es saber que existen y reconocer qué problema resuelve cada una.

Para comenzar, math, random, datetime, json y pathlib son excelentes opciones porque aparecen frecuentemente en proyectos reales y ejercicios.

¿Te sirvió esta guía? ☕

Si este contenido te ayudó y querés apoyar a Club Programador para seguir publicando ejercicios, proyectos y guías gratuitas, podés colaborar mediante:

☕ Apoyar con Mercado Pago

🌎 Apoyar con PayPal

Seguí aprendiendo Python

Encontrá todos los contenidos de Python en nuestra guía principal:

Aprender Python: guía completa paso a paso →

Proyecto avanzado en PSeInt: crear un Ta-Te-Ti contra la computadora con matrices y SubProcesos

En este proyecto vamos a crear un Ta-Te-Ti en PSeInt contra la computadora. El objetivo es practicar matrices, validaciones, Funciones, SubProcesos y lógica de decisiones construyendo un juego completo.

El jugador utilizará la ficha X y la computadora la ficha O. La CPU no jugará completamente al azar: primero intentará ganar, luego bloquear al jugador, después priorizará el centro y las esquinas.

¿Qué vamos a practicar?

  • matrices de 3 x 3;
  • recorridos con ciclos;
  • validación de posiciones;
  • Funciones y SubProcesos;
  • detección de victoria;
  • detección de empate;
  • lógica básica para una CPU;
  • uso de valores aleatorios.

Reglas del Ta-Te-Ti

El tablero tiene 9 posiciones. Cada jugador coloca una ficha por turno.

  • el jugador usa X;
  • la computadora usa O;
  • gana quien consiga tres fichas iguales en línea;
  • la línea puede ser horizontal, vertical o diagonal;
  • si se ocupan las 9 posiciones y nadie gana, hay empate.

Representar el tablero

Usaremos una matriz de 3 x 3:

Dimension tablero[3,3];

Cada casillero podrá contener:

" "  = libre
"X"  = jugador
"O"  = computadora

Código completo

SubProceso InicializarTablero(tablero Por Referencia)
    Definir fila, columna Como Entero;

    Para fila <- 1 Hasta 3 Hacer
        Para columna <- 1 Hasta 3 Hacer
            tablero[fila,columna] <- " ";
        FinPara
    FinPara
FinSubProceso


SubProceso MostrarTablero(tablero)
    Definir fila Como Entero;

    Limpiar Pantalla;

    Escribir "==========================";
    Escribir "        TA-TE-TI";
    Escribir "==========================";
    Escribir "";

    Para fila <- 1 Hasta 3 Hacer
        Escribir " ", tablero[fila,1], " | ", tablero[fila,2], " | ", tablero[fila,3];

        Si fila < 3 Entonces
            Escribir "---+---+---";
        FinSi
    FinPara

    Escribir "";
FinSubProceso


Funcion libre <- PosicionLibre(tablero, fila, columna)
    libre <- tablero[fila,columna] = " ";
FinFuncion


Funcion ganador <- HayGanador(tablero, ficha)
    Definir i Como Entero;

    ganador <- Falso;

    Para i <- 1 Hasta 3 Hacer
        Si tablero[i,1] = ficha Y tablero[i,2] = ficha Y tablero[i,3] = ficha Entonces
            ganador <- Verdadero;
        FinSi

        Si tablero[1,i] = ficha Y tablero[2,i] = ficha Y tablero[3,i] = ficha Entonces
            ganador <- Verdadero;
        FinSi
    FinPara

    Si tablero[1,1] = ficha Y tablero[2,2] = ficha Y tablero[3,3] = ficha Entonces
        ganador <- Verdadero;
    FinSi

    Si tablero[1,3] = ficha Y tablero[2,2] = ficha Y tablero[3,1] = ficha Entonces
        ganador <- Verdadero;
    FinSi
FinFuncion


Funcion lleno <- TableroLleno(tablero)
    Definir fila, columna Como Entero;

    lleno <- Verdadero;

    Para fila <- 1 Hasta 3 Hacer
        Para columna <- 1 Hasta 3 Hacer
            Si tablero[fila,columna] = " " Entonces
                lleno <- Falso;
            FinSi
        FinPara
    FinPara
FinFuncion


SubProceso TurnoJugador(tablero Por Referencia)
    Definir fila, columna Como Entero;
    Definir valida Como Logico;

    valida <- Falso;

    Mientras NO valida Hacer
        Escribir "Elegí una fila (1 a 3): ";
        Leer fila;

        Escribir "Elegí una columna (1 a 3): ";
        Leer columna;

        Si fila >= 1 Y fila <= 3 Y columna >= 1 Y columna <= 3 Entonces
            Si PosicionLibre(tablero, fila, columna) Entonces
                tablero[fila,columna] <- "X";
                valida <- Verdadero;
            SiNo
                Escribir "Esa posición ya está ocupada.";
            FinSi
        SiNo
            Escribir "Fila o columna inválida.";
        FinSi
    FinMientras
FinSubProceso


Funcion encontro <- BuscarJugada(tablero Por Referencia, ficha)
    Definir fila, columna Como Entero;

    encontro <- Falso;

    Para fila <- 1 Hasta 3 Hacer
        Para columna <- 1 Hasta 3 Hacer
            Si NO encontro Y tablero[fila,columna] = " " Entonces
                tablero[fila,columna] <- ficha;

                Si HayGanador(tablero, ficha) Entonces
                    encontro <- Verdadero;
                SiNo
                    tablero[fila,columna] <- " ";
                FinSi
            FinSi
        FinPara
    FinPara
FinFuncion


SubProceso TurnoCPU(tablero Por Referencia)
    Definir jugo Como Logico;
    Definir fila, columna, esquina, intentos Como Entero;

    jugo <- Falso;

    /// 1. Intentar ganar
    Si BuscarJugada(tablero, "O") Entonces
        jugo <- Verdadero;
    FinSi

    /// 2. Bloquear al jugador
    Si NO jugo Entonces
        Para fila <- 1 Hasta 3 Hacer
            Para columna <- 1 Hasta 3 Hacer
                Si NO jugo Y tablero[fila,columna] = " " Entonces
                    tablero[fila,columna] <- "X";

                    Si HayGanador(tablero, "X") Entonces
                        tablero[fila,columna] <- "O";
                        jugo <- Verdadero;
                    SiNo
                        tablero[fila,columna] <- " ";
                    FinSi
                FinSi
            FinPara
        FinPara
    FinSi

    /// 3. Priorizar el centro
    Si NO jugo Y tablero[2,2] = " " Entonces
        tablero[2,2] <- "O";
        jugo <- Verdadero;
    FinSi

    /// 4. Elegir una esquina libre
    Si NO jugo Entonces
        intentos <- 0;

        Mientras NO jugo Y intentos < 20 Hacer
            esquina <- Aleatorio(1,4);

            Segun esquina Hacer
                1:
                    fila <- 1;
                    columna <- 1;
                2:
                    fila <- 1;
                    columna <- 3;
                3:
                    fila <- 3;
                    columna <- 1;
                4:
                    fila <- 3;
                    columna <- 3;
            FinSegun

            Si tablero[fila,columna] = " " Entonces
                tablero[fila,columna] <- "O";
                jugo <- Verdadero;
            FinSi

            intentos <- intentos + 1;
        FinMientras
    FinSi

    /// 5. Elegir cualquier posición libre
    Si NO jugo Entonces
        Para fila <- 1 Hasta 3 Hacer
            Para columna <- 1 Hasta 3 Hacer
                Si NO jugo Y tablero[fila,columna] = " " Entonces
                    tablero[fila,columna] <- "O";
                    jugo <- Verdadero;
                FinSi
            FinPara
        FinPara
    FinSi
FinSubProceso


Proceso TaTeTi
    Definir tablero Como Cadena;
    Definir terminado Como Logico;

    Dimension tablero[3,3];

    InicializarTablero(tablero);

    terminado <- Falso;

    Mientras NO terminado Hacer
        MostrarTablero(tablero);

        TurnoJugador(tablero);

        Si HayGanador(tablero, "X") Entonces
            terminado <- Verdadero;
            MostrarTablero(tablero);
            Escribir "¡Ganaste!";
        SiNo
            Si TableroLleno(tablero) Entonces
                terminado <- Verdadero;
                MostrarTablero(tablero);
                Escribir "Empate.";
            SiNo
                TurnoCPU(tablero);

                Si HayGanador(tablero, "O") Entonces
                    terminado <- Verdadero;
                    MostrarTablero(tablero);
                    Escribir "Ganó la computadora.";
                SiNo
                    Si TableroLleno(tablero) Entonces
                        terminado <- Verdadero;
                        MostrarTablero(tablero);
                        Escribir "Empate.";
                    FinSi
                FinSi
            FinSi
        FinSi
    FinMientras
FinProceso

¿Cómo detectamos una victoria?

La función HayGanador() revisa:

  • las 3 filas;
  • las 3 columnas;
  • la diagonal principal;
  • la diagonal secundaria.

Si encuentra tres fichas iguales devuelve Verdadero.

La lógica de la computadora

La CPU sigue un orden de prioridades.

1. Intentar ganar

Primero prueba cada casillero libre colocando temporalmente una O. Si esa jugada produce una victoria, conserva la ficha.

2. Bloquear al jugador

Si no puede ganar, prueba posiciones libres con una X. Si descubre que el jugador ganaría en ese casillero, reemplaza esa ficha temporal por una O.

3. Tomar el centro

Si el centro está libre, la computadora lo elige porque es una de las mejores posiciones estratégicas.

4. Elegir una esquina

Si el centro no está disponible, intenta ocupar una de las cuatro esquinas.

5. Usar cualquier posición libre

Como último recurso utiliza el primer casillero que encuentre disponible.

¿Es una inteligencia artificial?

No en el sentido moderno del término. La computadora utiliza una serie de reglas programadas para decidir qué hacer.

Sin embargo, es un excelente ejercicio para introducir el concepto de toma de decisiones automática: analizar un estado, evaluar opciones y ejecutar una acción según prioridades.

Desafíos para mejorar el proyecto

  • permitir que el jugador elija X u O;
  • hacer que la computadora pueda comenzar la partida;
  • agregar marcador de partidas ganadas;
  • permitir jugar varias rondas;
  • mostrar números del 1 al 9 para elegir casillas;
  • crear niveles de dificultad;
  • mejorar la CPU para detectar jugadas estratégicas futuras;
  • implementar el algoritmo Minimax.

Desafío avanzado: Minimax

Una evolución interesante sería reemplazar las reglas actuales por el algoritmo Minimax. Ese algoritmo analiza las posibles jugadas futuras y permite construir una computadora que juegue Ta-Te-Ti de forma óptima.

Es un excelente siguiente paso para empezar a estudiar algoritmos relacionados con juegos e inteligencia artificial.

¿Te sirvió este proyecto? ☕

Si este contenido te ayudó y querés apoyar a Club Programador para seguir publicando ejercicios, juegos y guías gratuitas, podés colaborar mediante:

☕ Apoyar con Mercado Pago

🌎 Apoyar con PayPal

Seguí aprendiendo PSeInt

Encontrá todos los ejercicios, guías y proyectos en nuestra página principal de PSeInt:

Aprender PSeInt: guía completa y ejercicios resueltos →

20 ejercicios resueltos de DOM en JavaScript

20 ejercicios resueltos de DOM en JavaScript: eventos, formularios, listas, tabs, modales, filtros, tablas y más, explicados paso a paso.

El DOM (Document Object Model) permite que JavaScript lea y modifique el contenido de una página HTML. Con él podemos responder a eventos, cambiar textos, crear elementos, validar formularios y construir interfaces dinámicas.

En esta guía vas a practicar con 20 ejercicios resueltos de DOM en JavaScript, desde cambios simples de contenido hasta componentes como tabs, modales, listas de tareas, tablas y filtros en tiempo real.

Si necesitás repasar la teoría primero, consultá DOM en JavaScript: seleccionar, modificar y crear elementos HTML. También podés revisar 20 ejercicios resueltos de JavaScript para principiantes.

Cómo probar los ejercicios

Para practicar cómodamente, podés crear dos archivos: index.html y app.js. En index.html, vinculá JavaScript justo antes de cerrar </body>:

<script src="app.js"></script>

En cada ejercicio te mostramos el HTML base mínimo que necesita el ejemplo. Reemplazá el contenido del <body> por ese fragmento y copiá el JavaScript correspondiente dentro de app.js.

Cuando aparezca un bloque <style>, podés dejarlo dentro del HTML para probar rápidamente el ejemplo. En un proyecto real, lo habitual es mover esos estilos a un archivo CSS separado.

Índice de ejercicios

  1. Cambiar el texto de un elemento
  2. Cambiar el color de un elemento
  3. Mostrar y ocultar un elemento
  4. Contador de clics
  5. Agregar elementos a una lista
  6. Eliminar elementos de una lista
  7. Leer el valor de un input
  8. Validar un campo obligatorio
  9. Contador de caracteres
  10. Filtrar una lista en tiempo real
  11. Crear pestañas (tabs)
  12. Crear un acordeón
  13. Crear un modal
  14. Cambiar una imagen dinámicamente
  15. Crear una lista de tareas
  16. Marcar tareas como completadas
  17. Crear una tabla dinámica
  18. Ordenar una lista desde un botón
  19. Carrito básico en el DOM
  20. Mini proyecto: buscador con filtro y contador

1. Cambiar el texto de un elemento

Enunciado: Modificar el contenido de un párrafo al presionar un botón.

HTML base

<button id="cambiar">Cambiar texto</button>
<p id="texto">Texto original</p>

JavaScript

const boton = document.querySelector("#cambiar");
const texto = document.querySelector("#texto");

boton.addEventListener("click", function () {
    texto.textContent = "Texto actualizado";
});

querySelector() devuelve el primer elemento que coincide con el selector. textContent permite leer o cambiar su texto.

2. Cambiar el color de un elemento

Enunciado: Modificar el color de un título al hacer clic.

HTML base

<h2 id="titulo">Hacé clic sobre este título</h2>

JavaScript

const titulo = document.querySelector("#titulo");

titulo.addEventListener("click", function () {
    titulo.style.color = "red";
});

La propiedad style permite modificar estilos inline desde JavaScript. Para cambios más grandes suele ser mejor agregar o quitar clases CSS.

3. Mostrar y ocultar un elemento

Enunciado: Alternar la visibilidad de un bloque.

HTML base

<button id="toggle">Mostrar / ocultar</button>
<div id="caja">Contenido visible</div>

<style>
.oculto { display: none; }
</style>

JavaScript

const boton = document.querySelector("#toggle");
const caja = document.querySelector("#caja");

boton.addEventListener("click", function () {
    caja.classList.toggle("oculto");
});

classList.toggle() agrega la clase si no existe y la elimina si ya está presente.

4. Contador de clics

Enunciado: Contar cuántas veces se presiona un botón.

HTML base

<button id="contador">Contar clic</button>
<p id="salida">0</p>

JavaScript

const boton = document.querySelector("#contador");
const salida = document.querySelector("#salida");
let cantidad = 0;

boton.addEventListener("click", function () {
    cantidad++;
    salida.textContent = cantidad;
});

El evento click incrementa una variable y luego actualiza el DOM.

5. Agregar elementos a una lista

Enunciado: Crear nuevos elementos li dinámicamente.

HTML base

<button id="agregar">Agregar elemento</button>
<ul id="lista"></ul>

JavaScript

const lista = document.querySelector("#lista");
const boton = document.querySelector("#agregar");

boton.addEventListener("click", function () {
    const item = document.createElement("li");

    item.textContent = "Nuevo elemento";
    lista.appendChild(item);
});

createElement() crea un nodo y appendChild() lo agrega al final de otro elemento.

6. Eliminar elementos de una lista

Enunciado: Eliminar un elemento al presionar un botón dentro del mismo.

HTML base

<ul id="lista">
    <li>Elemento 1 <button class="eliminar">Eliminar</button></li>
    <li>Elemento 2 <button class="eliminar">Eliminar</button></li>
</ul>

JavaScript

const lista = document.querySelector("#lista");

lista.addEventListener("click", function (evento) {
    if (evento.target.matches(".eliminar")) {
        evento.target.parentElement.remove();
    }
});

Este ejemplo usa delegación de eventos: el listener está en la lista y detecta qué elemento originó el clic.

7. Leer el valor de un input

Enunciado: Mostrar lo escrito en un campo de texto.

HTML base

<input id="nombre" type="text" placeholder="Escribí tu nombre">
<button id="mostrar">Mostrar</button>
<p id="salida"></p>

JavaScript

const input = document.querySelector("#nombre");
const boton = document.querySelector("#mostrar");
const salida = document.querySelector("#salida");

boton.addEventListener("click", function () {
    salida.textContent = input.value;
});

La propiedad value devuelve el contenido actual de un campo de formulario.

8. Validar un campo obligatorio

Enunciado: Evitar aceptar un formulario si un campo está vacío.

HTML base

<form id="formulario">
    <input id="nombre" type="text" placeholder="Nombre">
    <button type="submit">Enviar</button>
</form>

<p id="mensaje"></p>

JavaScript

const formulario = document.querySelector("#formulario");
const nombre = document.querySelector("#nombre");
const mensaje = document.querySelector("#mensaje");

formulario.addEventListener("submit", function (evento) {
    evento.preventDefault();

    if (nombre.value.trim() === "") {
        mensaje.textContent = "El nombre es obligatorio";
        return;
    }

    mensaje.textContent = "Formulario válido";
});

preventDefault() evita el envío automático. trim() elimina espacios al principio y al final.

9. Contador de caracteres

Enunciado: Mostrar cuántos caracteres escribió el usuario.

HTML base

<textarea id="mensaje" placeholder="Escribí un mensaje"></textarea>
<p>Caracteres: <span id="contador">0</span></p>

JavaScript

const textarea = document.querySelector("#mensaje");
const contador = document.querySelector("#contador");

textarea.addEventListener("input", function () {
    contador.textContent = textarea.value.length;
});

El evento input se dispara cuando cambia el valor del campo, incluso al pegar texto.

10. Filtrar una lista en tiempo real

Enunciado: Mostrar sólo los elementos que coinciden con la búsqueda.

HTML base

<input id="buscar" type="text" placeholder="Buscar...">

<ul id="lista">
    <li>JavaScript</li>
    <li>Python</li>
    <li>PHP</li>
    <li>Laravel</li>
</ul>

JavaScript

const buscador = document.querySelector("#buscar");
const items = document.querySelectorAll("#lista li");

buscador.addEventListener("input", function () {
    const texto = buscador.value.toLowerCase();

    items.forEach(function (item) {
        const coincide = item.textContent.toLowerCase().includes(texto);

        item.style.display = coincide ? "" : "none";
    });
});

querySelectorAll() devuelve una colección de elementos. Luego usamos forEach() para evaluar cada uno.

11. Crear pestañas (tabs)

Enunciado: Cambiar el contenido visible según la pestaña seleccionada.

HTML base

<button class="tab" data-panel="panel1">Pestaña 1</button>
<button class="tab" data-panel="panel2">Pestaña 2</button>

<div id="panel1" class="panel">Contenido 1</div>
<div id="panel2" class="panel oculto">Contenido 2</div>

<style>
.oculto { display: none; }
</style>

JavaScript

const botones = document.querySelectorAll(".tab");
const paneles = document.querySelectorAll(".panel");

botones.forEach(function (boton) {
    boton.addEventListener("click", function () {
        paneles.forEach(function (panel) {
            panel.classList.add("oculto");
        });

        const id = boton.dataset.panel;
        document.querySelector("#" + id).classList.remove("oculto");
    });
});

dataset permite leer atributos data-*. Es útil para relacionar controles con elementos del DOM.

12. Crear un acordeón

Enunciado: Abrir y cerrar secciones de contenido.

HTML base

<button class="acordeon">Sección 1</button>
<div>Contenido de la sección 1</div>

<button class="acordeon">Sección 2</button>
<div class="oculto">Contenido de la sección 2</div>

<style>
.oculto { display: none; }
</style>

JavaScript

const botones = document.querySelectorAll(".acordeon");

botones.forEach(function (boton) {
    boton.addEventListener("click", function () {
        const contenido = boton.nextElementSibling;

        contenido.classList.toggle("oculto");
    });
});

nextElementSibling devuelve el siguiente elemento hermano dentro del DOM.

13. Crear un modal

Enunciado: Mostrar y cerrar una ventana modal.

HTML base

<button id="abrirModal">Abrir modal</button>

<div id="modal" class="oculto">
    <div>
        <p>Contenido del modal</p>
        <button id="cerrarModal">Cerrar</button>
    </div>
</div>

<style>
.oculto { display: none; }
</style>

JavaScript

const abrir = document.querySelector("#abrirModal");
const cerrar = document.querySelector("#cerrarModal");
const modal = document.querySelector("#modal");

abrir.addEventListener("click", function () {
    modal.classList.remove("oculto");
});

cerrar.addEventListener("click", function () {
    modal.classList.add("oculto");
});

El modal se controla cambiando una clase CSS. Esta técnica mantiene separadas la lógica y la presentación.

14. Cambiar una imagen dinámicamente

Enunciado: Modificar el atributo src de una imagen.

HTML base

<img id="imagen" src="imagen-1.jpg" alt="Imagen de ejemplo">
<button id="cambiarImagen">Cambiar imagen</button>

JavaScript

const imagen = document.querySelector("#imagen");
const boton = document.querySelector("#cambiarImagen");

boton.addEventListener("click", function () {
    imagen.setAttribute("src", "imagen-2.jpg");
});

setAttribute() modifica un atributo HTML. También podría escribirse directamente imagen.src = ....

15. Crear una lista de tareas

Enunciado: Agregar tareas desde un formulario.

HTML base

<form id="formTarea">
    <input id="tarea" type="text" placeholder="Nueva tarea">
    <button type="submit">Agregar</button>
</form>

<ul id="listaTareas"></ul>

JavaScript

const formulario = document.querySelector("#formTarea");
const input = document.querySelector("#tarea");
const lista = document.querySelector("#listaTareas");

formulario.addEventListener("submit", function (evento) {
    evento.preventDefault();

    const texto = input.value.trim();

    if (texto === "") {
        return;
    }

    const item = document.createElement("li");
    item.textContent = texto;

    lista.appendChild(item);
    input.value = "";
});

El ejemplo combina eventos, validación, creación de nodos y actualización del DOM.

16. Marcar tareas como completadas

Enunciado: Alternar el estado visual de una tarea.

HTML base

<ul id="listaTareas">
    <li>Estudiar JavaScript</li>
    <li>Practicar DOM</li>
</ul>

<style>
.completada {
    text-decoration: line-through;
    opacity: .6;
}
</style>

JavaScript

const lista = document.querySelector("#listaTareas");

lista.addEventListener("click", function (evento) {
    if (evento.target.matches("li")) {
        evento.target.classList.toggle("completada");
    }
});

La delegación de eventos permite que también funcionen las tareas creadas dinámicamente después de cargar la página.

17. Crear una tabla dinámica

Enunciado: Generar filas a partir de un array de objetos.

HTML base

<table id="tablaUsuarios">
    <thead>
        <tr>
            <th>Nombre</th>
            <th>Edad</th>
        </tr>
    </thead>
    <tbody></tbody>
</table>

JavaScript

const usuarios = [
    { nombre: "Ana", edad: 28 },
    { nombre: "Luis", edad: 34 },
    { nombre: "Marta", edad: 25 }
];

const cuerpo = document.querySelector("#tablaUsuarios tbody");

usuarios.forEach(function (usuario) {
    const fila = document.createElement("tr");

    fila.innerHTML =
        "<td>" + usuario.nombre + "</td>" +
        "<td>" + usuario.edad + "</td>";

    cuerpo.appendChild(fila);
});

innerHTML permite insertar HTML. Si los datos provienen de usuarios o fuentes externas, hay que evitar insertar contenido no confiable sin sanitizar.

18. Ordenar una lista desde un botón

Enunciado: Ordenar elementos alfabéticamente y volver a renderizarlos.

HTML base

<button id="ordenar">Ordenar</button>

<ul id="lista">
    <li>Python</li>
    <li>JavaScript</li>
    <li>CSS</li>
    <li>HTML</li>
</ul>

JavaScript

const lista = document.querySelector("#lista");
const boton = document.querySelector("#ordenar");

boton.addEventListener("click", function () {
    const items = [...lista.querySelectorAll("li")];

    items.sort(function (a, b) {
        return a.textContent.localeCompare(b.textContent);
    });

    items.forEach(function (item) {
        lista.appendChild(item);
    });
});

Convertimos la colección a array con ... para poder usar sort(). Al volver a agregar los nodos, el DOM cambia de orden.

19. Carrito básico en el DOM

Enunciado: Agregar productos a un carrito y actualizar el total.

HTML base

<button class="agregar" data-precio="25">Agregar Mouse ($25)</button>
<button class="agregar" data-precio="60">Agregar Teclado ($60)</button>

<p>Total: $<span id="total">0</span></p>

JavaScript

const botones = document.querySelectorAll(".agregar");
const totalElemento = document.querySelector("#total");
let total = 0;

botones.forEach(function (boton) {
    boton.addEventListener("click", function () {
        const precio = Number(boton.dataset.precio);

        total += precio;
        totalElemento.textContent = total;
    });
});

dataset.precio devuelve texto, por eso usamos Number() antes de sumarlo.

20. Mini proyecto: buscador con filtro y contador

Enunciado: Crear un buscador que filtre tarjetas y muestre cuántos resultados quedan visibles.

HTML base

<input id="buscar" type="text" placeholder="Buscar usuario...">

<p>Resultados: <span id="cantidad">0</span></p>

<div class="tarjeta">Ana García - ana@example.com</div>
<div class="tarjeta">Luis Pérez - luis@example.com</div>
<div class="tarjeta">Marta López - marta@example.com</div>

JavaScript

const buscador = document.querySelector("#buscar");
const tarjetas = document.querySelectorAll(".tarjeta");
const contador = document.querySelector("#cantidad");

function filtrar() {
    const texto = buscador.value.toLowerCase();
    let visibles = 0;

    tarjetas.forEach(function (tarjeta) {
        const coincide = tarjeta.textContent.toLowerCase().includes(texto);

        tarjeta.style.display = coincide ? "" : "none";

        if (coincide) {
            visibles++;
        }
    });

    contador.textContent = visibles;
}

buscador.addEventListener("input", filtrar);

filtrar();

El ejercicio combina búsqueda en tiempo real, recorrido de elementos, actualización visual y un contador de resultados.

Conceptos de DOM que practicamos

A lo largo de los ejercicios usamos querySelector(), querySelectorAll(), addEventListener(), textContent, value, classList, dataset, createElement(), appendChild(), innerHTML y delegación de eventos.

La idea es que no memorices cada línea, sino que identifiques el patrón: seleccionar un elemento, escuchar un evento, modificar el estado y reflejar ese cambio en el DOM.

Seguí aprendiendo JavaScript


¿Te sirvió esta guía? ☕

Si este contenido te ayudó a aprender JavaScript, podés apoyar a Club Programador. Cada aporte ayuda a mantener el sitio online y a seguir publicando material gratuito.

🇦🇷 Apoyar desde Argentina

☕ Invitame un café con Mercado Pago

🌎 Apoyar desde otro país

☕ Apoyar con PayPal

Proyecto avanzado en PSeInt: crear un juego de Batalla Naval con matrices y SubProcesos

En este proyecto vamos a crear un juego de Batalla Naval en PSeInt para un jugador contra la computadora. Es un proyecto ideal para practicar matrices, números aleatorios, Funciones, SubProcesos, validaciones y manejo de estados.

La idea es construir un juego completo, pero manteniendo el código suficientemente claro como para poder entender qué hace cada parte.

¿Cómo se juega a Batalla Naval?

Cada jugador tiene un tablero oculto donde coloca sus barcos. Durante la partida, ambos se turnan para disparar indicando una fila y una columna.

  • Si el disparo cae sobre agua, se marca como agua.
  • Si el disparo alcanza una parte de un barco, se marca como impacto.
  • Cuando todas las posiciones de un barco fueron alcanzadas, el barco queda hundido.
  • Gana quien hunda todos los barcos del rival.

La versión que vamos a programar

Para que la partida sea ágil vamos a utilizar un tablero de 8 x 8.

Cada jugador tendrá:

  • 1 barco de 3 casilleros;
  • 2 barcos de 2 casilleros;
  • 3 barcos de 1 casillero.

En total, cada jugador tendrá 6 barcos y 10 casilleros ocupados.

Los barcos se colocarán automáticamente en posiciones aleatorias y podrán estar en forma horizontal o vertical.

¿Cómo representaremos el tablero?

Usaremos una matriz de 8 filas por 8 columnas. Cada celda tendrá un número interno:

0  = agua sin disparar
1 a 6 = barco identificado por su número
10 = agua ya disparada
11 a 16 = parte impactada de los barcos 1 a 6

Este sistema tiene una ventaja importante: cuando una parte de un barco es alcanzada todavía podemos saber a qué barco pertenecía. Eso nos permite detectar cuándo quedó completamente hundido.

Mezcla y posiciones aleatorias

Para colocar los barcos usaremos Aleatorio(). El programa elegirá:

  • una fila inicial;
  • una columna inicial;
  • una orientación horizontal o vertical.

Antes de colocar el barco comprobaremos que entre completamente dentro del tablero y que no se superponga con otro.

Código completo

Funcion puede <- PuedeColocar(tablero, fila, columna, tamano, direccion)
    Definir i Como Entero;

    puede <- Verdadero;

    Si direccion = 1 Entonces
        Si columna + tamano - 1 > 8 Entonces
            puede <- Falso;
        SiNo
            Para i <- 0 Hasta tamano - 1 Hacer
                Si tablero[fila,columna + i] <> 0 Entonces
                    puede <- Falso;
                FinSi
            FinPara
        FinSi
    SiNo
        Si fila + tamano - 1 > 8 Entonces
            puede <- Falso;
        SiNo
            Para i <- 0 Hasta tamano - 1 Hacer
                Si tablero[fila + i,columna] <> 0 Entonces
                    puede <- Falso;
                FinSi
            FinPara
        FinSi
    FinSi
FinFuncion


Funcion hundido <- EstaHundido(tablero, idBarco)
    Definir fila, columna Como Entero;

    hundido <- Verdadero;

    Para fila <- 1 Hasta 8 Hacer
        Para columna <- 1 Hasta 8 Hacer
            Si tablero[fila,columna] = idBarco Entonces
                hundido <- Falso;
            FinSi
        FinPara
    FinPara
FinFuncion


Funcion quedan <- QuedanBarcos(tablero)
    Definir fila, columna Como Entero;

    quedan <- Falso;

    Para fila <- 1 Hasta 8 Hacer
        Para columna <- 1 Hasta 8 Hacer
            Si tablero[fila,columna] >= 1 Y tablero[fila,columna] <= 6 Entonces
                quedan <- Verdadero;
            FinSi
        FinPara
    FinPara
FinFuncion


SubProceso InicializarTablero(tablero Por Referencia)
    Definir fila, columna Como Entero;

    Para fila <- 1 Hasta 8 Hacer
        Para columna <- 1 Hasta 8 Hacer
            tablero[fila,columna] <- 0;
        FinPara
    FinPara
FinSubProceso


SubProceso ColocarUnBarco(tablero Por Referencia, idBarco, tamano)
    Definir fila, columna, direccion, i Como Entero;
    Definir colocado Como Logico;

    colocado <- Falso;

    Mientras NO colocado Hacer
        fila <- Aleatorio(1,8);
        columna <- Aleatorio(1,8);
        direccion <- Aleatorio(1,2);

        Si PuedeColocar(tablero, fila, columna, tamano, direccion) Entonces
            Si direccion = 1 Entonces
                Para i <- 0 Hasta tamano - 1 Hacer
                    tablero[fila,columna + i] <- idBarco;
                FinPara
            SiNo
                Para i <- 0 Hasta tamano - 1 Hacer
                    tablero[fila + i,columna] <- idBarco;
                FinPara
            FinSi

            colocado <- Verdadero;
        FinSi
    FinMientras
FinSubProceso


SubProceso ColocarFlota(tablero Por Referencia)
    ColocarUnBarco(tablero, 1, 3);
    ColocarUnBarco(tablero, 2, 2);
    ColocarUnBarco(tablero, 3, 2);
    ColocarUnBarco(tablero, 4, 1);
    ColocarUnBarco(tablero, 5, 1);
    ColocarUnBarco(tablero, 6, 1);
FinSubProceso


SubProceso MostrarTablero(tablero, ocultarBarcos)
    Definir fila, columna, valor Como Entero;

    Escribir "    1 2 3 4 5 6 7 8";

    Para fila <- 1 Hasta 8 Hacer
        Escribir Sin Saltar fila, " | ";

        Para columna <- 1 Hasta 8 Hacer
            valor <- tablero[fila,columna];

            Si valor = 0 Entonces
                Escribir Sin Saltar "~ ";
            SiNo
                Si valor >= 1 Y valor <= 6 Entonces
                    Si ocultarBarcos Entonces
                        Escribir Sin Saltar "~ ";
                    SiNo
                        Escribir Sin Saltar "B ";
                    FinSi
                SiNo
                    Si valor = 10 Entonces
                        Escribir Sin Saltar "O ";
                    SiNo
                        Escribir Sin Saltar "X ";
                    FinSi
                FinSi
            FinSi
        FinPara

        Escribir "";
    FinPara
FinSubProceso


SubProceso ProcesarDisparo(tablero Por Referencia, fila, columna, impacto Por Referencia, barcoHundido Por Referencia)
    Definir idBarco Como Entero;

    impacto <- Falso;
    barcoHundido <- Falso;

    Si tablero[fila,columna] = 0 Entonces
        tablero[fila,columna] <- 10;
    SiNo
        idBarco <- tablero[fila,columna];

        Si idBarco >= 1 Y idBarco <= 6 Entonces
            impacto <- Verdadero;
            tablero[fila,columna] <- idBarco + 10;

            Si EstaHundido(tablero, idBarco) Entonces
                barcoHundido <- Verdadero;
            FinSi
        FinSi
    FinSi
FinSubProceso


SubProceso TurnoJugador(tableroCPU Por Referencia)
    Definir fila, columna Como Entero;
    Definir valido, impacto, barcoHundido Como Logico;

    valido <- Falso;

    Mientras NO valido Hacer
        Escribir "Fila (1 a 8):";
        Leer fila;

        Escribir "Columna (1 a 8):";
        Leer columna;

        Si fila >= 1 Y fila <= 8 Y columna >= 1 Y columna <= 8 Entonces
            Si tableroCPU[fila,columna] < 10 Entonces
                valido <- Verdadero;
            SiNo
                Escribir "Ya disparaste en esa posición.";
            FinSi
        SiNo
            Escribir "Coordenadas inválidas.";
        FinSi
    FinMientras

    ProcesarDisparo(tableroCPU, fila, columna, impacto, barcoHundido);

    Si impacto Entonces
        Escribir "¡IMPACTO!";

        Si barcoHundido Entonces
            Escribir "¡HUNDISTE UN BARCO!";
        FinSi
    SiNo
        Escribir "Agua.";
    FinSi
FinSubProceso


SubProceso TurnoCPU(tableroJugador Por Referencia)
    Definir fila, columna Como Entero;
    Definir valido, impacto, barcoHundido Como Logico;

    valido <- Falso;

    Mientras NO valido Hacer
        fila <- Aleatorio(1,8);
        columna <- Aleatorio(1,8);

        Si tableroJugador[fila,columna] < 10 Entonces
            valido <- Verdadero;
        FinSi
    FinMientras

    Escribir "La computadora dispara a fila ", fila, ", columna ", columna;

    ProcesarDisparo(tableroJugador, fila, columna, impacto, barcoHundido);

    Si impacto Entonces
        Escribir "La computadora impactó uno de tus barcos.";

        Si barcoHundido Entonces
            Escribir "La computadora hundió uno de tus barcos.";
        FinSi
    SiNo
        Escribir "La computadora disparó al agua.";
    FinSi
FinSubProceso


Proceso Batalla_Naval
    Definir tableroJugador, tableroCPU Como Entero;
    Definir jugando Como Logico;

    Dimension tableroJugador[8,8];
    Dimension tableroCPU[8,8];

    InicializarTablero(tableroJugador);
    InicializarTablero(tableroCPU);

    ColocarFlota(tableroJugador);
    ColocarFlota(tableroCPU);

    jugando <- Verdadero;

    Mientras jugando Hacer
        Limpiar Pantalla;

        Escribir "====================================";
        Escribir "           BATALLA NAVAL";
        Escribir "====================================";

        Escribir "";
        Escribir "TU TABLERO";
        MostrarTablero(tableroJugador, Falso);

        Escribir "";
        Escribir "TABLERO ENEMIGO";
        MostrarTablero(tableroCPU, Verdadero);

        Escribir "";
        TurnoJugador(tableroCPU);

        Si NO QuedanBarcos(tableroCPU) Entonces
            Limpiar Pantalla;
            Escribir "====================================";
            Escribir "           ¡VICTORIA!";
            Escribir "====================================";
            Escribir "Hundiste toda la flota enemiga.";
            jugando <- Falso;
        SiNo
            Esperar Tecla;

            Escribir "";
            TurnoCPU(tableroJugador);

            Si NO QuedanBarcos(tableroJugador) Entonces
                Limpiar Pantalla;
                Escribir "====================================";
                Escribir "             DERROTA";
                Escribir "====================================";
                Escribir "La computadora hundió toda tu flota.";
                jugando <- Falso;
            SiNo
                Esperar Tecla;
            FinSi
        FinSi
    FinMientras
FinProceso

¿Cómo funciona la colocación de los barcos?

ColocarUnBarco() genera una posición y una orientación aleatorias. Después consulta a PuedeColocar().

La función comprueba dos cosas:

  • que el barco no se salga del tablero;
  • que todas las posiciones estén libres.

Si alguna condición falla, se generan nuevas coordenadas hasta encontrar una posición válida.

¿Cómo evitamos disparos repetidos?

Una posición que todavía no recibió disparos contiene agua (0) o el identificador de un barco (1 a 6).

Después de un disparo pasa a tener un valor igual o superior a 10. Por eso podemos verificar fácilmente si esa coordenada ya fue utilizada:

Si tablero[fila,columna] < 10 Entonces
    // La posición todavía no fue disparada
FinSi

¿Cómo sabemos si un barco fue hundido?

Cada barco posee un identificador entre 1 y 6. Cuando una parte es impactada, sumamos 10 a ese identificador.

Por ejemplo, si una celda tenía el valor 3, después del impacto pasa a valer 13.

EstaHundido() recorre toda la matriz buscando posiciones que todavía tengan el identificador original del barco. Si no encuentra ninguna, significa que todas sus partes fueron alcanzadas.

¿Cómo mostramos el tablero enemigo sin revelar sus barcos?

MostrarTablero() recibe el parámetro ocultarBarcos.

Cuando mostramos nuestro propio tablero, las posiciones con barcos aparecen con B. En el tablero de la computadora esas mismas posiciones se muestran como agua hasta que reciben un disparo.

Los símbolos utilizados son:

~ = posición desconocida o agua
B = uno de nuestros barcos
O = disparo al agua
X = impacto

La computadora

En esta versión la CPU dispara a una fila y columna aleatorias. Antes de hacerlo verifica que no haya disparado anteriormente a esa misma posición.

Esto hace que el rival sea funcional, aunque todavía no sea demasiado inteligente.

Ideas para mejorar la inteligencia de la CPU

Una mejora interesante sería hacer que, después de conseguir un impacto, la computadora pruebe primero las posiciones vecinas:

  • arriba;
  • abajo;
  • izquierda;
  • derecha.

De esta forma la CPU comenzaría a comportarse de manera parecida a un jugador real.

Otros desafíos para ampliar el juego

  • permitir al jugador colocar manualmente sus barcos;
  • usar un tablero de 10 x 10;
  • agregar barcos más grandes;
  • mostrar cuántos barcos quedan vivos;
  • llevar un contador de disparos;
  • calcular precisión: impactos / disparos;
  • hacer que quien acierta vuelva a disparar;
  • crear distintos niveles de dificultad;
  • crear una modalidad para dos jugadores humanos;
  • hacer una CPU que persiga un barco después del primer impacto.

Desafío

Modificá la computadora para que, cuando consiga un impacto, intente disparar alrededor de esa posición antes de volver a elegir coordenadas completamente al azar.

Después agregá un contador que indique cuántos disparos necesitó cada jugador para terminar la partida.

Repasos recomendados

Este proyecto utiliza matrices y SubProcesos de forma intensiva. Si necesitás repasar esos temas:

20 ejercicios resueltos de PSeInt con matrices →

Funciones y SubProcesos en PSeInt →

¿Te sirvió este proyecto? ☕

Si este contenido te ayudó y querés apoyar a Club Programador para seguir publicando ejercicios, juegos y guías gratuitas, podés colaborar mediante:

☕ Apoyar con Mercado Pago

🌎 Apoyar con PayPal

Seguí aprendiendo PSeInt

Encontrá todos los ejercicios, guías y proyectos en nuestra página principal de PSeInt:

Aprender PSeInt: guía completa y ejercicios resueltos →

Proyecto avanzado en PSeInt: crear un juego de Truco argentino con SubProcesos

En este proyecto vamos a crear un juego de Truco argentino en PSeInt para un jugador contra la computadora. Además de ser un juego divertido, es un ejercicio excelente para practicar Funciones, SubProcesos, vectores, números aleatorios, condicionales, ciclos y manejo de estados.

La idea es que el programa sea entendible incluso para alguien que nunca jugó al Truco. Por eso primero veremos las reglas que vamos a implementar y recién después construiremos el juego.

¿Qué versión del Truco vamos a programar?

El Truco argentino tiene muchas variantes. Para que el proyecto sea completo pero manejable, implementaremos:

  • 1 jugador contra la computadora;
  • mazo español de 40 cartas;
  • 3 cartas para cada jugador;
  • partida a 15 puntos;
  • Envido;
  • Truco;
  • Retruco;
  • Vale Cuatro;
  • reglas de parda;
  • alternancia de quién es mano;
  • mezcla aleatoria del mazo antes de cada mano.

Para esta primera versión dejaremos afuera Flor, Real Envido, Falta Envido y partidas de 2 contra 2. Son excelentes mejoras para agregar después.

Reglas básicas del Truco

El objetivo

El objetivo es ser el primero en llegar a 15 puntos. Cada mano permite ganar puntos mediante el Envido y mediante el Truco.

El mazo

Se utiliza un mazo español de 40 cartas. Los palos son:

  • Espada;
  • Basto;
  • Oro;
  • Copa.

En cada palo existen las cartas 1, 2, 3, 4, 5, 6, 7, 10, 11 y 12. No se utilizan 8 ni 9.

Reparto

Al comienzo de cada mano se mezclan las 40 cartas y se reparten 3 cartas a cada jugador. Una carta nunca puede repetirse porque primero mezclamos el mazo completo y luego tomamos las seis primeras posiciones.

Jerarquía de las cartas en el Truco

Una de las particularidades del Truco es que el valor numérico de una carta no determina directamente su fuerza. La jerarquía, de mayor a menor, es:

  1. 1 de Espada;
  2. 1 de Basto;
  3. 7 de Espada;
  4. 7 de Oro;
  5. todos los 3;
  6. todos los 2;
  7. 1 de Oro y 1 de Copa;
  8. todos los 12;
  9. todos los 11;
  10. todos los 10;
  11. 7 de Copa y 7 de Basto;
  12. todos los 6;
  13. todos los 5;
  14. todos los 4.

Para simplificar las comparaciones, nuestra función ValorTruco() transforma cada carta en un valor interno. Cuanto mayor sea ese valor, más fuerte será la carta.

¿Cómo se gana una mano?

Cada mano puede tener hasta tres vueltas. En cada vuelta ambos jugadores tiran una carta.

Normalmente gana la mano quien gana dos vueltas. Sin embargo, existen empates llamados pardas:

  • si la primera vuelta empata, quien gane la segunda gana la mano;
  • si alguien gana la primera y la segunda empata, gana quien había ganado la primera;
  • si cada jugador gana una de las dos primeras, la tercera decide;
  • si la tercera empata después de que cada uno ganó una vuelta, gana quien ganó la primera;
  • si todas las vueltas empatan, gana quien era mano.

Ser mano significa jugar primero al comienzo de esa mano. En nuestro programa esta condición alternará después de cada mano.

El Envido

El Envido compara combinaciones de cartas del mismo palo. Para calcularlo:

  • si tenemos dos cartas del mismo palo, sumamos sus valores y agregamos 20;
  • las figuras 10, 11 y 12 valen 0 para el Envido;
  • si no tenemos dos cartas del mismo palo, vale la carta numérica más alta entre 1 y 7;
  • si hay empate, gana quien es mano.

Ejemplos:

6 de Oro + 7 de Oro = 33 de Envido
1 de Copa + 12 de Copa = 21 de Envido
7 de Espada + 4 de Oro + 12 de Basto = 7 de Envido

En esta versión usamos solamente Envido: si es aceptado vale 2 puntos; si el rival no quiere, quien lo cantó recibe 1 punto.

Truco, Retruco y Vale Cuatro

La mano comienza valiendo 1 punto. Durante el juego puede aumentarse su valor:

  • Truco: la mano pasa a valer 2 puntos;
  • Retruco: pasa a valer 3 puntos;
  • Vale Cuatro: pasa a valer 4 puntos.

El rival puede aceptar o rechazar. Si rechaza, quien realizó el último canto gana el valor que tenía la mano antes de aumentar. Por ejemplo, rechazar un Truco entrega 1 punto; rechazar un Retruco entrega 2; rechazar un Vale Cuatro entrega 3.

Después de que un canto es aceptado, solamente el rival de quien realizó ese canto puede subir nuevamente la apuesta.

Representar las cartas con números

Internamente representaremos cada carta con un número del 1 al 40. Diez posiciones pertenecen a cada palo. Luego usaremos funciones para transformar ese número interno en el número y palo que ve el jugador.

Esto permite trabajar con un único vector:

Dimension mazo[40];

Para i <- 1 Hasta 40 Hacer
    mazo[i] <- i;
FinPara

Mezclar el mazo con Fisher-Yates

No conviene generar seis números aleatorios independientes porque podríamos obtener una carta repetida. En cambio, creamos las 40 cartas y mezclamos sus posiciones.

Usaremos una variante del algoritmo Fisher-Yates. En cada paso intercambiaremos una carta con una posición aleatoria de la parte del vector que todavía no fue procesada.

SubProceso MezclarMazo(mazo Por Referencia)
    Definir i, posicionAleatoria, auxiliar Como Entero;

    Para i <- 40 Hasta 2 Con Paso -1 Hacer
        posicionAleatoria <- Aleatorio(1,i);

        auxiliar <- mazo[i];
        mazo[i] <- mazo[posicionAleatoria];
        mazo[posicionAleatoria] <- auxiliar;
    FinPara
FinSubProceso

Después de mezclar, las cartas mazo[1] a mazo[3] pueden ir al jugador y mazo[4] a mazo[6] a la computadora.

Código completo del juego

Funcion numero <- NumeroCarta(carta)
    Definir posicion Como Entero;

    posicion <- carta - Trunc((carta - 1) / 10) * 10;

    Si posicion <= 7 Entonces
        numero <- posicion;
    SiNo
        numero <- posicion + 2;
    FinSi
FinFuncion


Funcion palo <- PaloCarta(carta)
    Definir codigo Como Entero;
    Definir palo Como Cadena;

    codigo <- Trunc((carta - 1) / 10) + 1;

    Segun codigo Hacer
        1:
            palo <- "Espada";
        2:
            palo <- "Basto";
        3:
            palo <- "Oro";
        4:
            palo <- "Copa";
    FinSegun
FinFuncion


Funcion valor <- ValorTruco(carta)
    Definir numero Como Entero;
    Definir palo Como Cadena;

    numero <- NumeroCarta(carta);
    palo <- PaloCarta(carta);

    Si numero = 1 Y palo = "Espada" Entonces
        valor <- 14;
    SiNo
        Si numero = 1 Y palo = "Basto" Entonces
            valor <- 13;
        SiNo
            Si numero = 7 Y palo = "Espada" Entonces
                valor <- 12;
            SiNo
                Si numero = 7 Y palo = "Oro" Entonces
                    valor <- 11;
                SiNo
                    Segun numero Hacer
                        3:
                            valor <- 10;
                        2:
                            valor <- 9;
                        1:
                            valor <- 8;
                        12:
                            valor <- 7;
                        11:
                            valor <- 6;
                        10:
                            valor <- 5;
                        7:
                            valor <- 4;
                        6:
                            valor <- 3;
                        5:
                            valor <- 2;
                        4:
                            valor <- 1;
                    FinSegun
                FinSi
            FinSi
        FinSi
    FinSi
FinFuncion


Funcion valor <- ValorEnvidoCarta(carta)
    Definir numero Como Entero;

    numero <- NumeroCarta(carta);

    Si numero >= 10 Entonces
        valor <- 0;
    SiNo
        valor <- numero;
    FinSi
FinFuncion


Funcion envido <- CalcularEnvido(mano)
    Definir v1, v2, v3, candidato Como Entero;
    Definir p1, p2, p3 Como Cadena;

    v1 <- ValorEnvidoCarta(mano[1]);
    v2 <- ValorEnvidoCarta(mano[2]);
    v3 <- ValorEnvidoCarta(mano[3]);

    p1 <- PaloCarta(mano[1]);
    p2 <- PaloCarta(mano[2]);
    p3 <- PaloCarta(mano[3]);

    envido <- v1;

    Si v2 > envido Entonces
        envido <- v2;
    FinSi

    Si v3 > envido Entonces
        envido <- v3;
    FinSi

    Si p1 = p2 Entonces
        candidato <- 20 + v1 + v2;

        Si candidato > envido Entonces
            envido <- candidato;
        FinSi
    FinSi

    Si p1 = p3 Entonces
        candidato <- 20 + v1 + v3;

        Si candidato > envido Entonces
            envido <- candidato;
        FinSi
    FinSi

    Si p2 = p3 Entonces
        candidato <- 20 + v2 + v3;

        Si candidato > envido Entonces
            envido <- candidato;
        FinSi
    FinSi
FinFuncion


Funcion ganador <- CompararCartas(cartaJugador, cartaCPU)
    Definir valorJugador, valorCPU Como Entero;

    valorJugador <- ValorTruco(cartaJugador);
    valorCPU <- ValorTruco(cartaCPU);

    Si valorJugador > valorCPU Entonces
        ganador <- 1;
    SiNo
        Si valorCPU > valorJugador Entonces
            ganador <- 2;
        SiNo
            ganador <- 0;
        FinSi
    FinSi
FinFuncion


Funcion nombre <- NombreCanto(nuevoValor)
    Definir nombre Como Cadena;

    Segun nuevoValor Hacer
        2:
            nombre <- "TRUCO";
        3:
            nombre <- "RETRUCO";
        4:
            nombre <- "VALE CUATRO";
    FinSegun
FinFuncion


SubProceso CrearMazo(mazo Por Referencia)
    Definir i Como Entero;

    Para i <- 1 Hasta 40 Hacer
        mazo[i] <- i;
    FinPara
FinSubProceso


SubProceso MezclarMazo(mazo Por Referencia)
    Definir i, posicionAleatoria, auxiliar Como Entero;

    Para i <- 40 Hasta 2 Con Paso -1 Hacer
        posicionAleatoria <- Aleatorio(1,i);

        auxiliar <- mazo[i];
        mazo[i] <- mazo[posicionAleatoria];
        mazo[posicionAleatoria] <- auxiliar;
    FinPara
FinSubProceso


SubProceso Repartir(mazo, manoJugador Por Referencia, manoCPU Por Referencia)
    manoJugador[1] <- mazo[1];
    manoJugador[2] <- mazo[2];
    manoJugador[3] <- mazo[3];

    manoCPU[1] <- mazo[4];
    manoCPU[2] <- mazo[5];
    manoCPU[3] <- mazo[6];
FinSubProceso


SubProceso MostrarCarta(carta)
    Escribir Sin Saltar NumeroCarta(carta), " de ", PaloCarta(carta);
FinSubProceso


SubProceso MostrarMano(manoJugador, usadaJugador)
    Definir i Como Entero;

    Escribir "";
    Escribir "Tus cartas:";

    Para i <- 1 Hasta 3 Hacer
        Si NO usadaJugador[i] Entonces
            Escribir Sin Saltar i, ". ";
            MostrarCarta(manoJugador[i]);
            Escribir "";
        FinSi
    FinPara
FinSubProceso


Funcion posicion <- ElegirCartaJugador(manoJugador, usadaJugador)
    Repetir
        MostrarMano(manoJugador, usadaJugador);
        Escribir "Elegí la posición de la carta que querés jugar:";
        Leer posicion;
    Hasta Que posicion >= 1 Y posicion <= 3 Y NO usadaJugador[posicion]
FinFuncion


Funcion posicion <- ElegirCartaCPU(usadaCPU)
    Repetir
        posicion <- Aleatorio(1,3);
    Hasta Que NO usadaCPU[posicion]
FinFuncion


Funcion aceptar <- CPUAceptaTruco(manoCPU)
    Definir i, mejorCarta, probabilidad Como Entero;

    mejorCarta <- 0;

    Para i <- 1 Hasta 3 Hacer
        Si ValorTruco(manoCPU[i]) > mejorCarta Entonces
            mejorCarta <- ValorTruco(manoCPU[i]);
        FinSi
    FinPara

    probabilidad <- Aleatorio(1,100);

    Si mejorCarta >= 11 Entonces
        aceptar <- probabilidad <= 85;
    SiNo
        Si mejorCarta >= 8 Entonces
            aceptar <- probabilidad <= 65;
        SiNo
            aceptar <- probabilidad <= 40;
        FinSi
    FinSi
FinFuncion


SubProceso GestionarEnvido(manoJugador, manoCPU, mano, puntosJugador Por Referencia, puntosCPU Por Referencia)
    Definir opcion, envidoJugador, envidoCPU, probabilidad Como Entero;
    Definir acepta, cantado Como Logico;

    cantado <- Falso;
    envidoJugador <- CalcularEnvido(manoJugador);
    envidoCPU <- CalcularEnvido(manoCPU);

    Escribir "";
    Escribir "¿Querés cantar ENVIDO?";
    Escribir "1. Sí";
    Escribir "2. No";
    Leer opcion;

    Si opcion = 1 Entonces
        cantado <- Verdadero;

        probabilidad <- Aleatorio(1,100);

        Si envidoCPU >= 27 Entonces
            acepta <- Verdadero;
        SiNo
            acepta <- probabilidad <= 55;
        FinSi

        Si acepta Entonces
            Escribir "La computadora dice: ¡QUIERO!";
        SiNo
            Escribir "La computadora dice: NO QUIERO.";
            puntosJugador <- puntosJugador + 1;
        FinSi
    SiNo
        Si envidoCPU >= 27 O Aleatorio(1,100) <= 25 Entonces
            cantado <- Verdadero;

            Escribir "La computadora canta: ¡ENVIDO!";
            Escribir "1. Quiero";
            Escribir "2. No quiero";
            Leer opcion;

            Si opcion = 1 Entonces
                acepta <- Verdadero;
            SiNo
                acepta <- Falso;
                puntosCPU <- puntosCPU + 1;
            FinSi
        SiNo
            acepta <- Falso;
        FinSi
    FinSi

    Si cantado Y acepta Entonces
        Escribir "Tu Envido: ", envidoJugador;
        Escribir "Envido de la computadora: ", envidoCPU;

        Si envidoJugador > envidoCPU Entonces
            puntosJugador <- puntosJugador + 2;
            Escribir "Ganaste el Envido.";
        SiNo
            Si envidoCPU > envidoJugador Entonces
                puntosCPU <- puntosCPU + 2;
                Escribir "La computadora ganó el Envido.";
            SiNo
                Si mano = 1 Entonces
                    puntosJugador <- puntosJugador + 2;
                    Escribir "Empate de Envido: ganás por ser mano.";
                SiNo
                    puntosCPU <- puntosCPU + 2;
                    Escribir "Empate de Envido: gana la computadora por ser mano.";
                FinSi
            FinSi
        FinSi

        Esperar Tecla;
    FinSi
FinSubProceso


SubProceso IntentarCantoJugador(manoCPU, apuestaTruco Por Referencia, ultimoCantor Por Referencia, puntosJugador Por Referencia, manoTerminada Por Referencia)
    Definir nuevoValor Como Entero;

    nuevoValor <- apuestaTruco + 1;

    Escribir "Cantás: ¡", NombreCanto(nuevoValor), "!";

    Si CPUAceptaTruco(manoCPU) Entonces
        Escribir "La computadora dice: ¡QUIERO!";
        apuestaTruco <- nuevoValor;
        ultimoCantor <- 1;
    SiNo
        Escribir "La computadora dice: NO QUIERO.";
        puntosJugador <- puntosJugador + apuestaTruco;
        manoTerminada <- Verdadero;
    FinSi
FinSubProceso


SubProceso IntentarCantoCPU(apuestaTruco Por Referencia, ultimoCantor Por Referencia, puntosCPU Por Referencia, manoTerminada Por Referencia)
    Definir nuevoValor, opcion Como Entero;

    nuevoValor <- apuestaTruco + 1;

    Escribir "La computadora canta: ¡", NombreCanto(nuevoValor), "!";
    Escribir "1. Quiero";
    Escribir "2. No quiero";
    Leer opcion;

    Si opcion = 1 Entonces
        apuestaTruco <- nuevoValor;
        ultimoCantor <- 2;
    SiNo
        puntosCPU <- puntosCPU + apuestaTruco;
        manoTerminada <- Verdadero;
    FinSi
FinSubProceso


Funcion ganador <- ResolverDespuesDeDos(g1, g2)
    ganador <- -1;

    Si g1 = 0 Y g2 <> 0 Entonces
        ganador <- g2;
    FinSi

    Si g1 <> 0 Y g2 = 0 Entonces
        ganador <- g1;
    FinSi

    Si g1 <> 0 Y g1 = g2 Entonces
        ganador <- g1;
    FinSi
FinFuncion


Funcion ganador <- ResolverConTercera(g1, g2, g3, mano)
    Si g1 = 0 Y g2 = 0 Entonces
        Si g3 = 0 Entonces
            ganador <- mano;
        SiNo
            ganador <- g3;
        FinSi
    SiNo
        Si g1 <> 0 Y g2 <> 0 Y g1 <> g2 Entonces
            Si g3 = 0 Entonces
                ganador <- g1;
            SiNo
                ganador <- g3;
            FinSi
        SiNo
            ganador <- ResolverDespuesDeDos(g1,g2);
        FinSi
    FinSi
FinFuncion


Proceso Truco_Argentino
    Definir mazo, manoJugador, manoCPU Como Entero;
    Definir usadaJugador, usadaCPU Como Logico;
    Definir puntosJugador, puntosCPU, mano Como Entero;
    Definir apuestaTruco, ultimoCantor, vuelta, lider Como Entero;
    Definir posJugador, posCPU, cartaJugador, cartaCPU Como Entero;
    Definir g1, g2, g3, ganadorVuelta, ganadorMano Como Entero;
    Definir opcion, i Como Entero;
    Definir manoTerminada Como Logico;

    Dimension mazo[40];
    Dimension manoJugador[3];
    Dimension manoCPU[3];
    Dimension usadaJugador[3];
    Dimension usadaCPU[3];

    puntosJugador <- 0;
    puntosCPU <- 0;
    mano <- 1;

    Mientras puntosJugador < 15 Y puntosCPU < 15 Hacer
        CrearMazo(mazo);
        MezclarMazo(mazo);
        Repartir(mazo, manoJugador, manoCPU);

        Para i <- 1 Hasta 3 Hacer
            usadaJugador[i] <- Falso;
            usadaCPU[i] <- Falso;
        FinPara

        apuestaTruco <- 1;
        ultimoCantor <- 0;
        manoTerminada <- Falso;
        g1 <- 0;
        g2 <- 0;
        g3 <- 0;

        Limpiar Pantalla;
        Escribir "====================================";
        Escribir "          TRUCO ARGENTINO";
        Escribir "====================================";
        Escribir "Vos: ", puntosJugador, " puntos";
        Escribir "CPU: ", puntosCPU, " puntos";

        Si mano = 1 Entonces
            Escribir "Sos mano.";
        SiNo
            Escribir "La computadora es mano.";
        FinSi

        MostrarMano(manoJugador, usadaJugador);
        GestionarEnvido(manoJugador, manoCPU, mano, puntosJugador, puntosCPU);

        lider <- mano;
        vuelta <- 1;

        Mientras vuelta <= 3 Y NO manoTerminada Hacer
            Limpiar Pantalla;

            Escribir "Marcador: Vos ", puntosJugador, " - CPU ", puntosCPU;
            Escribir "Valor actual de la mano: ", apuestaTruco;
            Escribir "Vuelta ", vuelta;

            Si lider = 1 Entonces
                MostrarMano(manoJugador, usadaJugador);

                Si apuestaTruco < 4 Y (ultimoCantor = 0 O ultimoCantor = 2) Entonces
                    Escribir "1. Jugar carta";
                    Escribir "2. Cantar ", NombreCanto(apuestaTruco + 1);
                    Leer opcion;

                    Si opcion = 2 Entonces
                        IntentarCantoJugador(manoCPU, apuestaTruco, ultimoCantor, puntosJugador, manoTerminada);
                    FinSi
                FinSi

                Si NO manoTerminada Entonces
                    posJugador <- ElegirCartaJugador(manoJugador, usadaJugador);
                    cartaJugador <- manoJugador[posJugador];
                    usadaJugador[posJugador] <- Verdadero;

                    Escribir Sin Saltar "Jugaste: ";
                    MostrarCarta(cartaJugador);
                    Escribir "";

                    Si apuestaTruco < 4 Y (ultimoCantor = 0 O ultimoCantor = 1) Y Aleatorio(1,100) <= 18 Entonces
                        IntentarCantoCPU(apuestaTruco, ultimoCantor, puntosCPU, manoTerminada);
                    FinSi
                FinSi

                Si NO manoTerminada Entonces
                    posCPU <- ElegirCartaCPU(usadaCPU);
                    cartaCPU <- manoCPU[posCPU];
                    usadaCPU[posCPU] <- Verdadero;

                    Escribir Sin Saltar "CPU juega: ";
                    MostrarCarta(cartaCPU);
                    Escribir "";
                FinSi
            SiNo
                Si apuestaTruco < 4 Y (ultimoCantor = 0 O ultimoCantor = 1) Y Aleatorio(1,100) <= 18 Entonces
                    IntentarCantoCPU(apuestaTruco, ultimoCantor, puntosCPU, manoTerminada);
                FinSi

                Si NO manoTerminada Entonces
                    posCPU <- ElegirCartaCPU(usadaCPU);
                    cartaCPU <- manoCPU[posCPU];
                    usadaCPU[posCPU] <- Verdadero;

                    Escribir Sin Saltar "CPU juega: ";
                    MostrarCarta(cartaCPU);
                    Escribir "";

                    MostrarMano(manoJugador, usadaJugador);

                    Si apuestaTruco < 4 Y (ultimoCantor = 0 O ultimoCantor = 2) Entonces
                        Escribir "1. Jugar carta";
                        Escribir "2. Cantar ", NombreCanto(apuestaTruco + 1);
                        Leer opcion;

                        Si opcion = 2 Entonces
                            IntentarCantoJugador(manoCPU, apuestaTruco, ultimoCantor, puntosJugador, manoTerminada);
                        FinSi
                    FinSi
                FinSi

                Si NO manoTerminada Entonces
                    posJugador <- ElegirCartaJugador(manoJugador, usadaJugador);
                    cartaJugador <- manoJugador[posJugador];
                    usadaJugador[posJugador] <- Verdadero;

                    Escribir Sin Saltar "Jugaste: ";
                    MostrarCarta(cartaJugador);
                    Escribir "";
                FinSi
            FinSi

            Si NO manoTerminada Entonces
                ganadorVuelta <- CompararCartas(cartaJugador, cartaCPU);

                Si ganadorVuelta = 1 Entonces
                    Escribir "Ganaste la vuelta.";
                    lider <- 1;
                SiNo
                    Si ganadorVuelta = 2 Entonces
                        Escribir "La CPU ganó la vuelta.";
                        lider <- 2;
                    SiNo
                        Escribir "Parda.";
                    FinSi
                FinSi

                Si vuelta = 1 Entonces
                    g1 <- ganadorVuelta;
                FinSi

                Si vuelta = 2 Entonces
                    g2 <- ganadorVuelta;
                    ganadorMano <- ResolverDespuesDeDos(g1,g2);

                    Si ganadorMano <> -1 Entonces
                        manoTerminada <- Verdadero;
                    FinSi
                FinSi

                Si vuelta = 3 Entonces
                    g3 <- ganadorVuelta;
                    ganadorMano <- ResolverConTercera(g1,g2,g3,mano);
                    manoTerminada <- Verdadero;
                FinSi

                vuelta <- vuelta + 1;
                Esperar Tecla;
            FinSi
        FinMientras

        Si ganadorMano = 1 Entonces
            puntosJugador <- puntosJugador + apuestaTruco;
            Escribir "Ganaste la mano y sumás ", apuestaTruco, " punto(s).";
        FinSi

        Si ganadorMano = 2 Entonces
            puntosCPU <- puntosCPU + apuestaTruco;
            Escribir "La CPU ganó la mano y suma ", apuestaTruco, " punto(s).";
        FinSi

        Si mano = 1 Entonces
            mano <- 2;
        SiNo
            mano <- 1;
        FinSi

        Esperar Tecla;
    FinMientras

    Limpiar Pantalla;
    Escribir "====================================";
    Escribir "             FIN DEL JUEGO";
    Escribir "====================================";

    Si puntosJugador >= 15 Entonces
        Escribir "¡Ganaste la partida!";
    SiNo
        Escribir "La computadora ganó la partida.";
    FinSi

    Escribir "Resultado final: ", puntosJugador, " a ", puntosCPU;
FinProceso

¿Qué hace cada parte?

NumeroCarta() y PaloCarta()

Estas funciones traducen el número interno del 1 al 40 a una carta real. De esta manera el resto del programa puede trabajar con enteros sin perder una presentación comprensible para el jugador.

ValorTruco()

Convierte cada carta a un valor de fuerza. Esto evita llenar el programa de comparaciones especiales cada vez que queremos decidir quién ganó una vuelta.

CalcularEnvido()

Analiza las tres cartas de una mano. Primero obtiene el valor individual de cada una y luego compara las tres posibles parejas para verificar si comparten palo.

MezclarMazo()

Usa Aleatorio() para implementar Fisher-Yates. Como solamente cambiamos posiciones de un mazo ya existente, no hay riesgo de generar cartas repetidas.

ResolverDespuesDeDos() y ResolverConTercera()

Estas funciones concentran las reglas de las pardas. Separarlas del proceso principal hace que la lógica de quién gana una mano sea mucho más fácil de leer y modificar.

La computadora

La CPU elige cartas disponibles al azar, pero algunas decisiones tienen una pequeña lógica. Por ejemplo, es más probable que acepte un Truco si posee al menos una carta fuerte y tiende a cantar Envido cuando su puntaje es alto.

No estamos intentando construir una inteligencia artificial perfecta: queremos una computadora suficientemente impredecible como para que el juego resulte entretenido y, al mismo tiempo, mantener el código accesible.

Un detalle importante sobre el azar

Aleatorio() aparece en varias partes del proyecto:

  • mezcla del mazo;
  • selección de cartas de la CPU;
  • decisión de aceptar o rechazar ciertos cantos;
  • decisión de la computadora de cantar Truco.

Por eso dos partidas no deberían desarrollarse exactamente igual.

Ideas para mejorar el proyecto

  • agregar Real Envido y Falta Envido;
  • implementar Flor;
  • jugar a 30 puntos;
  • hacer una CPU que analice las cartas ya jugadas;
  • permitir que la computadora mienta o “farolee” con más frecuencia;
  • guardar estadísticas de partidas ganadas y perdidas;
  • crear una versión para 2 jugadores humanos;
  • crear la modalidad 2 contra 2;
  • separar todavía más la lógica en nuevos SubProcesos.

Desafío

Como primer desafío, modificá la lógica de la computadora para que no elija siempre una carta completamente al azar. Por ejemplo, podría intentar jugar una carta baja cuando ya sabe que no puede ganar una vuelta o reservar su mejor carta para más adelante.

Después agregá Real Envido y Falta Envido. Ahí vas a tener que transformar el Envido en un verdadero sistema de apuestas encadenadas, de manera similar a lo que ya hicimos con Truco, Retruco y Vale Cuatro.

¿Te sirvió este proyecto? ☕

Si este juego te resultó útil y querés apoyar a Club Programador para que podamos seguir publicando ejercicios, proyectos y guías gratuitas, podés colaborar mediante:

☕ Apoyar con Mercado Pago

🌎 Apoyar con PayPal

Cualquier aporte ayuda a seguir creando contenido gratuito para aprender programación.

Seguí aprendiendo PSeInt

Este proyecto combina prácticamente todos los conceptos que fuimos viendo en la serie. Si necesitás repasar alguno, podés volver a la guía principal:

Aprender PSeInt: guía completa y ejercicios resueltos →

Proyecto avanzado en PSeInt: crear un juego RPG por turnos completo

En este proyecto vamos a construir un juego RPG por turnos completo en PSeInt. La idea no es solamente hacer un juego largo: vamos a usarlo para practicar cómo dividir un problema grande en partes más pequeñas mediante SubProcesos y Funciones.

El jugador podrá elegir una clase, combatir enemigos aleatorios, usar pociones, defenderse, comprar mejoras, ganar experiencia, subir de nivel y enfrentarse finalmente a un jefe.

¿Qué vamos a practicar?

  • variables y tipos de datos;
  • condicionales y estructuras Segun;
  • ciclos Mientras;
  • números aleatorios;
  • parámetros por valor y por referencia;
  • SubProcesos y Funciones;
  • manejo del estado de un juego;
  • separación de responsabilidades.

1. La idea del juego

El juego comienza creando un héroe. Cada clase tiene estadísticas diferentes:

  • Guerrero: mucha vida y defensa.
  • Mago: menos vida, pero mucho ataque.
  • Arquero: estadísticas más equilibradas.

Después de cada combate el jugador recibe oro y experiencia. Entre batallas puede visitar una tienda, comprar pociones o mejorar sus estadísticas. Al conseguir cinco victorias aparece el jefe final.

2. ¿Por qué modularizar?

Podríamos escribir todo dentro del proceso principal, pero el código rápidamente se volvería difícil de leer. Por eso separaremos tareas concretas como mostrar el estado del héroe, generar enemigos, calcular daño, administrar la tienda o subir de nivel.

Una Función resulta útil cuando necesitamos calcular y devolver un valor. Un SubProceso es conveniente cuando queremos ejecutar una tarea más amplia y, si hace falta, modificar variables recibidas Por Referencia.

3. Código completo

Copiá el siguiente código en PSeInt y ejecutalo. Después vamos a repasar las partes más importantes.

Funcion resultado <- CalcularDanio(ataque, defensa)
    Definir resultado Como Entero;

    resultado <- ataque + Aleatorio(1,6) - defensa;

    Si resultado < 1 Entonces
        resultado <- 1;
    FinSi
FinFuncion


SubProceso MostrarEstado(nombre, clase, nivel, vida, vidaMaxima, ataque, defensa, experiencia, experienciaNecesaria, oro, pociones, combatesGanados)
    Limpiar Pantalla;

    Escribir "====================================";
    Escribir "        ESTADO DEL HEROE";
    Escribir "====================================";
    Escribir "Nombre: ", nombre;
    Escribir "Clase: ", clase;
    Escribir "Nivel: ", nivel;
    Escribir "Vida: ", vida, "/", vidaMaxima;
    Escribir "Ataque: ", ataque;
    Escribir "Defensa: ", defensa;
    Escribir "Experiencia: ", experiencia, "/", experienciaNecesaria;
    Escribir "Oro: ", oro;
    Escribir "Pociones: ", pociones;
    Escribir "Combates ganados: ", combatesGanados;
FinSubProceso


SubProceso CrearPersonaje(opcion, clase Por Referencia, vidaMaxima Por Referencia, ataque Por Referencia, defensa Por Referencia)
    Segun opcion Hacer
        1:
            clase <- "Guerrero";
            vidaMaxima <- 130;
            ataque <- 18;
            defensa <- 8;

        2:
            clase <- "Mago";
            vidaMaxima <- 90;
            ataque <- 25;
            defensa <- 4;

        3:
            clase <- "Arquero";
            vidaMaxima <- 105;
            ataque <- 21;
            defensa <- 6;

        De Otro Modo:
            clase <- "Guerrero";
            vidaMaxima <- 130;
            ataque <- 18;
            defensa <- 8;
    FinSegun
FinSubProceso


SubProceso CrearEnemigo(nivel, combatesGanados, nombreEnemigo Por Referencia, vidaEnemigo Por Referencia, ataqueEnemigo Por Referencia, defensaEnemigo Por Referencia)
    Definir enemigo Como Entero;

    Si combatesGanados >= 5 Entonces
        nombreEnemigo <- "Dragon Oscuro";
        vidaEnemigo <- 180 + nivel * 20;
        ataqueEnemigo <- 22 + nivel * 3;
        defensaEnemigo <- 8 + nivel;
    SiNo
        enemigo <- Aleatorio(1,4);

        Segun enemigo Hacer
            1:
                nombreEnemigo <- "Goblin";
                vidaEnemigo <- 45 + nivel * 10;
                ataqueEnemigo <- 10 + nivel * 2;
                defensaEnemigo <- 2 + nivel;

            2:
                nombreEnemigo <- "Esqueleto";
                vidaEnemigo <- 55 + nivel * 10;
                ataqueEnemigo <- 12 + nivel * 2;
                defensaEnemigo <- 3 + nivel;

            3:
                nombreEnemigo <- "Orco";
                vidaEnemigo <- 75 + nivel * 12;
                ataqueEnemigo <- 15 + nivel * 2;
                defensaEnemigo <- 5 + nivel;

            4:
                nombreEnemigo <- "Hechicero oscuro";
                vidaEnemigo <- 60 + nivel * 10;
                ataqueEnemigo <- 18 + nivel * 2;
                defensaEnemigo <- 3 + nivel;
        FinSegun
    FinSi
FinSubProceso


SubProceso SubirNivel(experiencia Por Referencia, experienciaNecesaria Por Referencia, nivel Por Referencia, vida Por Referencia, vidaMaxima Por Referencia, ataque Por Referencia, defensa Por Referencia)
    Mientras experiencia >= experienciaNecesaria Hacer
        experiencia <- experiencia - experienciaNecesaria;
        nivel <- nivel + 1;
        experienciaNecesaria <- experienciaNecesaria + 50;

        vidaMaxima <- vidaMaxima + 20;
        ataque <- ataque + 4;
        defensa <- defensa + 2;
        vida <- vidaMaxima;

        Escribir "";
        Escribir "====================================";
        Escribir "        ¡SUBISTE DE NIVEL!";
        Escribir "====================================";
        Escribir "Ahora sos nivel ", nivel;
        Escribir "Tu vida, ataque y defensa aumentaron.";
    FinMientras
FinSubProceso


SubProceso Tienda(oro Por Referencia, pociones Por Referencia, ataque Por Referencia, defensa Por Referencia)
    Definir opcion Como Entero;

    Limpiar Pantalla;
    Escribir "====================================";
    Escribir "              TIENDA";
    Escribir "====================================";
    Escribir "Oro disponible: ", oro;
    Escribir "";
    Escribir "1. Poción - 20 oro";
    Escribir "2. Mejorar ataque - 50 oro";
    Escribir "3. Mejorar defensa - 50 oro";
    Escribir "4. Volver";
    Leer opcion;

    Segun opcion Hacer
        1:
            Si oro >= 20 Entonces
                oro <- oro - 20;
                pociones <- pociones + 1;
                Escribir "Compraste una poción.";
            SiNo
                Escribir "No tenés suficiente oro.";
            FinSi;

        2:
            Si oro >= 50 Entonces
                oro <- oro - 50;
                ataque <- ataque + 3;
                Escribir "Tu ataque aumentó.";
            SiNo
                Escribir "No tenés suficiente oro.";
            FinSi;

        3:
            Si oro >= 50 Entonces
                oro <- oro - 50;
                defensa <- defensa + 2;
                Escribir "Tu defensa aumentó.";
            SiNo
                Escribir "No tenés suficiente oro.";
            FinSi;
    FinSegun

    Esperar Tecla;
FinSubProceso


SubProceso Combatir(nombre, clase, vida Por Referencia, vidaMaxima, ataque, defensa, pociones Por Referencia, nombreEnemigo, vidaEnemigo Por Referencia, ataqueEnemigo, defensaEnemigo)
    Definir opcion, danio, danioEnemigo, probabilidad Como Entero;

    Mientras vida > 0 Y vidaEnemigo > 0 Hacer
        Limpiar Pantalla;

        Escribir "====================================";
        Escribir nombre, " VS ", nombreEnemigo;
        Escribir "====================================";
        Escribir nombre, ": ", vida, "/", vidaMaxima, " HP";
        Escribir nombreEnemigo, ": ", vidaEnemigo, " HP";
        Escribir "";
        Escribir "1. Atacar";
        Escribir "2. Ataque especial";
        Escribir "3. Defender";
        Escribir "4. Usar poción";
        Leer opcion;

        Segun opcion Hacer
            1:
                danio <- CalcularDanio(ataque, defensaEnemigo);
                probabilidad <- Aleatorio(1,100);

                Si probabilidad <= 15 Entonces
                    danio <- danio * 2;
                    Escribir "¡GOLPE CRITICO!";
                FinSi

                vidaEnemigo <- vidaEnemigo - danio;
                Escribir "Causás ", danio, " puntos de daño.";

            2:
                Segun clase Hacer
                    "Guerrero":
                        danio <- ataque * 2 - defensaEnemigo;
                        Escribir "¡GOLPE DEVASTADOR!";

                    "Mago":
                        danio <- ataque * 2 + Aleatorio(5,15);
                        Escribir "¡BOLA DE FUEGO!";

                    "Arquero":
                        danio <- ataque + Aleatorio(10,20);
                        Escribir "¡FLECHA PRECISA!";
                FinSegun

                Si danio < 1 Entonces
                    danio <- 1;
                FinSi

                vidaEnemigo <- vidaEnemigo - danio;
                Escribir "Causás ", danio, " puntos de daño.";

            3:
                Escribir "Te preparás para recibir el ataque.";

            4:
                Si pociones > 0 Entonces
                    vida <- vida + 40;

                    Si vida > vidaMaxima Entonces
                        vida <- vidaMaxima;
                    FinSi

                    pociones <- pociones - 1;
                    Escribir "Usaste una poción. Vida actual: ", vida;
                SiNo
                    Escribir "No te quedan pociones.";
                FinSi;
        FinSegun

        Si vidaEnemigo > 0 Entonces
            danioEnemigo <- CalcularDanio(ataqueEnemigo, defensa);

            Si opcion = 3 Entonces
                danioEnemigo <- Trunc(danioEnemigo / 2);
            FinSi

            vida <- vida - danioEnemigo;
            Escribir nombreEnemigo, " te causa ", danioEnemigo, " de daño.";
        FinSi

        Esperar Tecla;
    FinMientras
FinSubProceso


Proceso RPG_ClubProgramador
    Definir nombre, clase, nombreEnemigo Como Cadena;
    Definir vida, vidaMaxima, ataque, defensa Como Entero;
    Definir nivel, experiencia, experienciaNecesaria Como Entero;
    Definir oro, pociones, combatesGanados Como Entero;
    Definir vidaEnemigo, ataqueEnemigo, defensaEnemigo Como Entero;
    Definir opcion Como Entero;
    Definir jugando Como Logico;

    Limpiar Pantalla;

    Escribir "====================================";
    Escribir "       REINOS DE PSEINT";
    Escribir "====================================";
    Escribir "¿Cómo se llama tu héroe?";
    Leer nombre;

    Escribir "";
    Escribir "Elegí una clase:";
    Escribir "1. Guerrero";
    Escribir "2. Mago";
    Escribir "3. Arquero";
    Leer opcion;

    CrearPersonaje(opcion, clase, vidaMaxima, ataque, defensa);

    vida <- vidaMaxima;
    nivel <- 1;
    experiencia <- 0;
    experienciaNecesaria <- 100;
    oro <- 30;
    pociones <- 3;
    combatesGanados <- 0;
    jugando <- Verdadero;

    Mientras jugando Hacer
        MostrarEstado(nombre, clase, nivel, vida, vidaMaxima, ataque, defensa, experiencia, experienciaNecesaria, oro, pociones, combatesGanados);

        Escribir "";
        Escribir "1. Buscar enemigo";
        Escribir "2. Ir a la tienda";
        Escribir "3. Descansar";
        Escribir "4. Salir";
        Leer opcion;

        Segun opcion Hacer
            1:
                CrearEnemigo(nivel, combatesGanados, nombreEnemigo, vidaEnemigo, ataqueEnemigo, defensaEnemigo);

                Escribir "";
                Escribir "¡APARECE ", nombreEnemigo, "!";
                Esperar Tecla;

                Combatir(nombre, clase, vida, vidaMaxima, ataque, defensa, pociones, nombreEnemigo, vidaEnemigo, ataqueEnemigo, defensaEnemigo);

                Si vida <= 0 Entonces
                    Limpiar Pantalla;
                    Escribir "====================================";
                    Escribir "             GAME OVER";
                    Escribir "====================================";
                    Escribir nombre, " ha sido derrotado.";
                    Escribir "Nivel alcanzado: ", nivel;
                    Escribir "Combates ganados: ", combatesGanados;
                    jugando <- Falso;
                SiNo
                    Si nombreEnemigo = "Dragon Oscuro" Entonces
                        Limpiar Pantalla;
                        Escribir "====================================";
                        Escribir "        ¡VICTORIA FINAL!";
                        Escribir "====================================";
                        Escribir nombre, " derrotó al Dragon Oscuro.";
                        Escribir "El reino vuelve a estar a salvo.";
                        jugando <- Falso;
                    SiNo
                        combatesGanados <- combatesGanados + 1;
                        oro <- oro + Aleatorio(15,35);
                        experiencia <- experiencia + Aleatorio(35,60);

                        Escribir "¡Victoria! Ganaste oro y experiencia.";

                        SubirNivel(experiencia, experienciaNecesaria, nivel, vida, vidaMaxima, ataque, defensa);
                        Esperar Tecla;
                    FinSi
                FinSi;

            2:
                Tienda(oro, pociones, ataque, defensa);

            3:
                vida <- vidaMaxima;
                Escribir "Descansaste y recuperaste toda tu vida.";
                Esperar Tecla;

            4:
                jugando <- Falso;

            De Otro Modo:
                Escribir "Opción inválida.";
                Esperar Tecla;
        FinSegun
    FinMientras

    Escribir "";
    Escribir "Gracias por jugar.";
FinProceso

4. La función CalcularDanio

CalcularDanio() recibe el ataque de quien golpea y la defensa de quien recibe el golpe. Agregamos un pequeño valor aleatorio para que los ataques no causen exactamente el mismo daño en cada turno.

La función devuelve un número entero y garantiza un mínimo de 1 punto de daño. Es un buen ejemplo de una tarea pequeña que conviene aislar porque se utiliza tanto para el jugador como para los enemigos.

5. Parámetros Por Referencia

En varios SubProcesos usamos Por Referencia. Esto permite que una modificación realizada dentro del SubProceso afecte directamente a la variable original.

Por ejemplo, la tienda puede descontar oro y aumentar el ataque:

Tienda(oro, pociones, ataque, defensa);

Como esas variables se reciben Por Referencia, los cambios siguen existiendo cuando volvemos al proceso principal.

6. Generación de enemigos

CrearEnemigo() usa Aleatorio(1,4) para elegir uno de cuatro enemigos. Sus estadísticas además dependen del nivel actual del jugador, por lo que los combates se vuelven progresivamente más difíciles.

Cuando el jugador alcanza cinco victorias dejamos de generar enemigos normales y aparece el Dragón Oscuro.

7. El sistema de combate

Mientras tanto el jugador como el enemigo tengan vida, el combate continúa. Cada turno permite:

  • realizar un ataque normal;
  • usar una habilidad especial propia de la clase;
  • defenderse para reducir el siguiente daño;
  • usar una poción.

Los ataques normales tienen además un 15 % de probabilidad de convertirse en golpes críticos y causar el doble de daño.

8. Experiencia y niveles

Después de cada victoria se obtiene experiencia. Cuando alcanza el valor necesario se ejecuta SubirNivel().

Al subir de nivel aumentan la vida máxima, el ataque y la defensa. También restauramos completamente la vida del personaje.

Usamos un ciclo Mientras dentro del SubProceso para contemplar incluso el caso en que una recompensa grande permita subir más de un nivel.

9. La tienda

El oro ganado puede invertirse en:

  • pociones;
  • mejoras de ataque;
  • mejoras de defensa.

Esta mecánica introduce una pequeña decisión estratégica: guardar oro para mejorar estadísticas o gastar antes para tener más recursos durante los combates.

10. Ideas para seguir mejorándolo

El juego todavía puede crecer mucho. Algunos desafíos interesantes son:

  • agregar maná y hacer que los ataques especiales tengan un costo;
  • incorporar enfriamiento para las habilidades especiales;
  • crear más clases;
  • agregar armaduras y armas;
  • incorporar distintos jefes;
  • crear probabilidades de esquivar ataques;
  • agregar estados como veneno o quemadura;
  • crear un inventario con vectores;
  • guardar puntuaciones de diferentes jugadores;
  • incorporar un mapa o mazmorra usando matrices.

Desafío

Intentá agregar una cuarta clase llamada Paladín. Debería tener mucha defensa y una habilidad especial que, además de causar daño, recupere una pequeña cantidad de vida.

Después intentá agregar un enemigo nuevo con una habilidad especial propia.

Seguí aprendiendo PSeInt

Si todavía no trabajaste con SubProcesos y Funciones, te conviene revisar primero nuestra guía:

Funciones y SubProcesos en PSeInt →

También podés encontrar todos los ejercicios y proyectos en la guía principal:

Aprender PSeInt: guía completa y ejercicios resueltos →

20 ejercicios avanzados de Python resueltos paso a paso

20 ejercicios avanzados de Python resueltos paso a paso: listas, diccionarios, matrices, búsqueda, ordenamiento, validaciones y pequeños sistemas prácticos.

Después de dominar variables, condicionales, ciclos, listas y funciones, el siguiente paso es aprender a combinar esos conceptos para resolver problemas más completos.

Estos 20 ejercicios avanzados de Python resueltos paso a paso están pensados para practicar procesamiento de datos, estructuras anidadas, búsquedas, ordenamientos, matrices, validaciones y pequeños sistemas sin introducir todavía conceptos de programación orientada a objetos como herencia.

Si necesitás repasar antes, podés comenzar por 20 ejercicios resueltos de Python para principiantes, 20 ejercicios de listas en Python o Funciones en Python.

Índice de ejercicios

  1. Contar la frecuencia de palabras
  2. Normalizar una lista de nombres
  3. Agrupar alumnos por curso
  4. Ordenar registros por más de un criterio
  5. Eliminar duplicados conservando el orden
  6. Comparar dos listas y detectar cambios
  7. Generar estadísticas de una lista
  8. Unir dos listas por identificador
  9. Calcular totales de una matriz
  10. Trasponer una matriz
  11. Validar una contraseña con varias reglas
  12. Cifrado César
  13. Detectar palíndromos en frases
  14. Validar paréntesis balanceados
  15. Buscar varias coincidencias con búsqueda binaria
  16. Ordenar registros sin usar sort()
  17. Crear un sistema de inventario
  18. Calcular el total de un carrito con descuentos
  19. Procesar datos separados por comas
  20. Mini proyecto: gestor de productos

1. Contar la frecuencia de palabras

Enunciado: Contar cuántas veces aparece cada palabra en un texto ignorando mayúsculas y signos simples.

texto = "Python es simple. Python es potente."

texto = texto.lower().replace(".", "")
palabras = texto.split()

frecuencias = {}

for palabra in palabras:
    if palabra in frecuencias:
        frecuencias[palabra] += 1
    else:
        frecuencias[palabra] = 1

print(frecuencias)

lower() normaliza el texto y split() lo divide en palabras. El diccionario guarda cada palabra como clave y su cantidad de apariciones como valor.

2. Normalizar una lista de nombres

Enunciado: Quitar espacios sobrantes y dejar todos los nombres con formato consistente.

nombres = ["  ana", "LUIS ", "  mArTa  "]
normalizados = []

for nombre in nombres:
    limpio = nombre.strip().title()
    normalizados.append(limpio)

print(normalizados)

strip() elimina espacios al principio y al final. title() devuelve una nueva cadena con formato de título.

3. Agrupar alumnos por curso

Enunciado: Crear un diccionario donde cada curso contenga su lista de alumnos.

alumnos = [
    {"nombre": "Ana", "curso": "A"},
    {"nombre": "Luis", "curso": "B"},
    {"nombre": "Marta", "curso": "A"}
]

grupos = {}

for alumno in alumnos:
    curso = alumno["curso"]

    if curso not in grupos:
        grupos[curso] = []

    grupos[curso].append(alumno["nombre"])

print(grupos)

Este patrón permite transformar una lista plana en información agrupada. Es muy útil para reportes y resúmenes.

4. Ordenar registros por más de un criterio

Enunciado: Ordenar alumnos por nota descendente y luego por nombre.

alumnos = [
    {"nombre": "Luis", "nota": 8},
    {"nombre": "Ana", "nota": 9},
    {"nombre": "Carlos", "nota": 8}
]

def criterio(alumno):
    return (-alumno["nota"], alumno["nombre"])

ordenados = sorted(alumnos, key=criterio)

for alumno in ordenados:
    print(alumno)

sorted() devuelve una nueva lista. La función usada en key define el criterio de ordenamiento sin modificar la lista original.

5. Eliminar duplicados conservando el orden

Enunciado: Quitar valores repetidos sin alterar el orden de aparición.

datos = [4, 2, 4, 7, 2, 9, 7]
unicos = []

for dato in datos:
    if dato not in unicos:
        unicos.append(dato)

print(unicos)

A diferencia de convertir directamente a set, esta solución hace explícito que queremos conservar el orden original.

6. Comparar dos listas y detectar cambios

Enunciado: Obtener elementos agregados y eliminados entre dos versiones.

anterior = [1, 2, 3, 4]
actual = [2, 3, 4, 5]

agregados = []
eliminados = []

for elemento in actual:
    if elemento not in anterior:
        agregados.append(elemento)

for elemento in anterior:
    if elemento not in actual:
        eliminados.append(elemento)

print("Agregados:", agregados)
print("Eliminados:", eliminados)

Comparar estados anteriores y actuales aparece en sincronizaciones, formularios y procesos de actualización.

7. Generar estadísticas de una lista

Enunciado: Calcular mínimo, máximo, promedio y cantidad de valores.

numeros = [12, 7, 19, 4, 15, 10]

estadisticas = {
    "cantidad": len(numeros),
    "minimo": min(numeros),
    "maximo": max(numeros),
    "promedio": sum(numeros) / len(numeros)
}

print(estadisticas)

min(), max() y sum() permiten resolver operaciones habituales sobre colecciones numéricas.

8. Unir dos listas por identificador

Enunciado: Combinar información proveniente de dos listas relacionadas por id.

usuarios = [
    {"id": 1, "nombre": "Ana"},
    {"id": 2, "nombre": "Luis"}
]

edades = [
    {"usuario_id": 1, "edad": 28},
    {"usuario_id": 2, "edad": 34}
]

resultado = []

for usuario in usuarios:
    registro = usuario.copy()

    for dato in edades:
        if dato["usuario_id"] == usuario["id"]:
            registro["edad"] = dato["edad"]
            break

    resultado.append(registro)

print(resultado)

Este ejercicio reproduce de forma simple una operación parecida a un join: relacionamos registros mediante un identificador común.

9. Calcular totales de una matriz

Enunciado: Calcular la suma de cada fila y de cada columna.

matriz = [
    [3, 5, 2],
    [7, 1, 4],
    [6, 8, 9]
]

for fila in matriz:
    print("Total fila:", sum(fila))

cantidad_columnas = len(matriz[0])

for columna in range(cantidad_columnas):
    total = 0

    for fila in matriz:
        total += fila[columna]

    print("Total columna", columna, ":", total)

Las matrices pueden representarse como listas de listas. Para recorrer columnas necesitamos mantener fijo el índice de columna y avanzar por cada fila.

10. Trasponer una matriz

Enunciado: Convertir filas en columnas.

matriz = [
    [1, 2, 3],
    [4, 5, 6]
]

traspuesta = []

for columna in range(len(matriz[0])):
    nueva_fila = []

    for fila in matriz:
        nueva_fila.append(fila[columna])

    traspuesta.append(nueva_fila)

print(traspuesta)

La traspuesta intercambia filas por columnas. Es un buen ejercicio para practicar índices y estructuras bidimensionales.

11. Validar una contraseña con varias reglas

Enunciado: Comprobar longitud, mayúsculas, minúsculas y números.

clave = "Python2026"

tiene_mayuscula = False
tiene_minuscula = False
tiene_numero = False

for caracter in clave:
    if caracter.isupper():
        tiene_mayuscula = True
    elif caracter.islower():
        tiene_minuscula = True
    elif caracter.isdigit():
        tiene_numero = True

valida = (
    len(clave) >= 8
    and tiene_mayuscula
    and tiene_minuscula
    and tiene_numero
)

print("Contraseña válida:", valida)

El ejercicio combina recorrido de cadenas, varias condiciones y estado acumulado mediante variables booleanas.

12. Cifrado César

Enunciado: Desplazar cada letra una cantidad fija de posiciones.

texto = "hola"
desplazamiento = 3
resultado = ""

for caracter in texto:
    codigo = ord(caracter)
    nuevo_codigo = codigo + desplazamiento
    resultado += chr(nuevo_codigo)

print(resultado)

ord() devuelve el código numérico de un carácter y chr() realiza la operación inversa. Esta versión simple está pensada para practicar la lógica del desplazamiento.

13. Detectar palíndromos en frases

Enunciado: Comprobar una frase ignorando espacios y mayúsculas.

frase = "Anita lava la tina"

normalizada = frase.lower().replace(" ", "")
invertida = normalizada[::-1]

if normalizada == invertida:
    print("Es palíndromo")
else:
    print("No es palíndromo")

El slicing [::-1] crea una versión invertida de la cadena. Antes normalizamos el contenido para que la comparación sea útil.

14. Validar paréntesis balanceados

Enunciado: Comprobar si los paréntesis de una expresión están correctamente abiertos y cerrados.

expresion = "(a + b) * (c - d)"
pila = []
valida = True

for caracter in expresion:
    if caracter == "(":
        pila.append(caracter)
    elif caracter == ")":
        if len(pila) == 0:
            valida = False
            break

        pila.pop()

if len(pila) > 0:
    valida = False

print("Expresión válida:", valida)

La lista funciona como una pila: append() agrega una apertura y pop() retira la última cuando aparece un cierre.

15. Buscar varias coincidencias con búsqueda binaria

Enunciado: Encontrar todas las posiciones de un valor repetido en una lista ordenada.

numeros = [1, 2, 2, 2, 4, 7, 9]
buscado = 2

izquierda = 0
derecha = len(numeros) - 1
encontrado = -1

while izquierda <= derecha:
    medio = (izquierda + derecha) // 2

    if numeros[medio] == buscado:
        encontrado = medio
        break
    elif numeros[medio] < buscado:
        izquierda = medio + 1
    else:
        derecha = medio - 1

posiciones = []

if encontrado != -1:
    i = encontrado

    while i >= 0 and numeros[i] == buscado:
        posiciones.append(i)
        i -= 1

    i = encontrado + 1

    while i < len(numeros) and numeros[i] == buscado:
        posiciones.append(i)
        i += 1

posiciones.sort()

print(posiciones)

Primero aplicamos búsqueda binaria para encontrar una coincidencia y después recorremos hacia ambos lados para localizar las repeticiones.

16. Ordenar registros sin usar sort()

Enunciado: Implementar selección para ordenar productos por precio.

productos = [
    {"nombre": "Mouse", "precio": 30},
    {"nombre": "Teclado", "precio": 80},
    {"nombre": "Monitor", "precio": 250}
]

for i in range(len(productos)):
    menor = i

    for j in range(i + 1, len(productos)):
        if productos[j]["precio"] < productos[menor]["precio"]:
            menor = j

    productos[i], productos[menor] = productos[menor], productos[i]

print(productos)

El algoritmo de selección busca el menor elemento restante y lo coloca en su posición. Aquí adaptamos el criterio para trabajar con diccionarios.

17. Crear un sistema de inventario

Enunciado: Agregar productos, actualizar stock, buscar por código y calcular el valor total.

inventario = []

def buscar_producto(codigo):
    for producto in inventario:
        if producto["codigo"] == codigo:
            return producto

    return None

def agregar_producto(codigo, nombre, precio, stock):
    if buscar_producto(codigo) is None:
        inventario.append({
            "codigo": codigo,
            "nombre": nombre,
            "precio": precio,
            "stock": stock
        })

def actualizar_stock(codigo, cantidad):
    producto = buscar_producto(codigo)

    if producto is not None:
        producto["stock"] += cantidad

def valor_total():
    total = 0

    for producto in inventario:
        total += producto["precio"] * producto["stock"]

    return total

agregar_producto("A1", "Mouse", 25, 10)
agregar_producto("B2", "Teclado", 60, 5)
actualizar_stock("A1", 3)

print(inventario)
print("Valor total:", valor_total())

El problema combina funciones reutilizables, listas de diccionarios, búsquedas y acumuladores sin introducir programación orientada a objetos.

18. Calcular el total de un carrito con descuentos

Enunciado: Aplicar descuentos según cantidad y calcular el total final.

carrito = [
    {"producto": "Mouse", "precio": 25, "cantidad": 2},
    {"producto": "Teclado", "precio": 60, "cantidad": 5}
]

total = 0

for item in carrito:
    subtotal = item["precio"] * item["cantidad"]

    if item["cantidad"] >= 5:
        subtotal *= 0.9

    total += subtotal

print("Total:", round(total, 2))

Este tipo de ejercicio combina reglas de negocio, cálculos y estructuras de datos. round() permite limitar la cantidad de decimales del resultado.

19. Procesar datos separados por comas

Enunciado: Interpretar texto con formato similar a CSV y calcular un resumen.

datos = """Ana,8
Luis,6
Marta,9
Pedro,7"""

lineas = datos.splitlines()
alumnos = []

for linea in lineas:
    partes = linea.split(",")

    alumnos.append({
        "nombre": partes[0],
        "nota": int(partes[1])
    })

total = 0

for alumno in alumnos:
    total += alumno["nota"]

promedio = total / len(alumnos)

print(alumnos)
print("Promedio:", promedio)

Antes de trabajar con archivos reales, este ejercicio permite practicar el procesamiento de información tabular usando sólo cadenas, listas y conversiones.

20. Mini proyecto: gestor de productos

Enunciado: Crear un pequeño sistema con altas, búsqueda, actualización y reportes.

productos = []

def agregar(codigo, nombre, precio, stock):
    for producto in productos:
        if producto["codigo"] == codigo:
            return False

    productos.append({
        "codigo": codigo,
        "nombre": nombre,
        "precio": precio,
        "stock": stock
    })

    return True

def buscar(codigo):
    for producto in productos:
        if producto["codigo"] == codigo:
            return producto

    return None

def cambiar_precio(codigo, nuevo_precio):
    producto = buscar(codigo)

    if producto is None:
        return False

    producto["precio"] = nuevo_precio
    return True

def productos_bajo_stock(limite):
    resultado = []

    for producto in productos:
        if producto["stock"] < limite:
            resultado.append(producto)

    return resultado

def valor_inventario():
    total = 0

    for producto in productos:
        total += producto["precio"] * producto["stock"]

    return total

agregar("P1", "Mouse", 25, 4)
agregar("P2", "Teclado", 70, 12)
agregar("P3", "Monitor", 250, 2)

cambiar_precio("P1", 30)

print("Bajo stock:", productos_bajo_stock(5))
print("Valor inventario:", valor_inventario())

El proyecto reúne funciones, validaciones, búsquedas, listas, diccionarios y cálculos. Es una buena forma de practicar cómo dividir un problema grande en operaciones pequeñas.

Qué estamos practicando

La dificultad de estos ejercicios no está en usar sintaxis especialmente rara, sino en combinar herramientas que ya conocés: funciones, listas, diccionarios, cadenas, ciclos, condiciones, búsqueda y ordenamiento.

Cuando un problema empieza a crecer, una buena estrategia es dividirlo en funciones pequeñas, evitar repetir lógica y elegir una estructura de datos que represente claramente la información.

Seguí aprendiendo Python


¿Te sirvió esta guía? ☕

Si este contenido te ayudó a aprender Python, podés apoyar a Club Programador. Cada aporte ayuda a mantener el sitio online y a seguir publicando material gratuito.

🇦🇷 Apoyar desde Argentina

☕ Invitame un café con Mercado Pago

🌎 Apoyar desde otro país

☕ Apoyar con PayPal

20 ejercicios avanzados de JavaScript resueltos paso a paso

20 ejercicios avanzados de JavaScript resueltos paso a paso: reduce, filtros, paginación, debounce, throttle, Promises, fetch, APIs, localStorage y más.

Si ya dominás variables, condicionales, ciclos, funciones y arrays, estos 20 ejercicios avanzados de JavaScript te permiten practicar problemas más cercanos a aplicaciones reales.

La guía combina manipulación de datos, reduce(), ordenamiento, paginación, asincronía, fetch(), Promises, localStorage, patrones como debounce y throttle, y un pequeño proyecto final.

Antes de empezar, podés repasar 20 ejercicios resueltos de JavaScript para principiantes y 20 ejercicios resueltos de arrays en JavaScript.

Índice de ejercicios

  1. Contar la frecuencia de palabras en un texto
  2. Agrupar objetos por una propiedad con reduce()
  3. Ordenar objetos por múltiples criterios
  4. Crear una búsqueda con varios filtros
  5. Implementar paginación de un array
  6. Crear un carrito de compras
  7. Calcular subtotal, descuento e impuestos
  8. Transformar datos anidados de una API
  9. Eliminar objetos duplicados por id
  10. Comparar dos arrays y detectar cambios
  11. Crear una función debounce()
  12. Crear una función throttle()
  13. Ejecutar tareas asíncronas en secuencia
  14. Ejecutar varias Promises en paralelo
  15. Manejar errores con async/await
  16. Consumir una API y filtrar resultados
  17. Reintentar una petición fallida
  18. Guardar objetos complejos en localStorage
  19. Crear un sistema de eventos personalizado
  20. Mini proyecto: buscador de usuarios desde una API

1. Contar la frecuencia de palabras en un texto

Enunciado: Contar cuántas veces aparece cada palabra ignorando mayúsculas y signos básicos.

const texto = "JavaScript es potente. JavaScript es flexible.";

const palabras = texto
    .toLowerCase()
    .replace(/[.,]/g, "")
    .split(" ");

const frecuencia = {};

for (const palabra of palabras) {
    frecuencia[palabra] = (frecuencia[palabra] || 0) + 1;
}

console.log(frecuencia);

toLowerCase() normaliza el texto, replace() elimina signos simples y split() crea un array de palabras. Después usamos un objeto como tabla de frecuencias.

2. Agrupar objetos por una propiedad con reduce()

Enunciado: Agrupar productos por categoría utilizando reduce().

const productos = [
    { nombre: "Mouse", categoria: "tecnologia" },
    { nombre: "Silla", categoria: "muebles" },
    { nombre: "Teclado", categoria: "tecnologia" }
];

const agrupados = productos.reduce(function (resultado, producto) {
    const categoria = producto.categoria;

    if (!resultado[categoria]) {
        resultado[categoria] = [];
    }

    resultado[categoria].push(producto);

    return resultado;
}, {});

console.log(agrupados);

reduce() transforma el array completo en una única estructura. En este caso, el resultado es un objeto cuyas propiedades son las categorías.

3. Ordenar objetos por múltiples criterios

Enunciado: Ordenar personas primero por edad y, en caso de empate, por nombre.

const personas = [
    { nombre: "Luis", edad: 30 },
    { nombre: "Ana", edad: 25 },
    { nombre: "Carlos", edad: 30 }
];

personas.sort(function (a, b) {
    if (a.edad !== b.edad) {
        return a.edad - b.edad;
    }

    return a.nombre.localeCompare(b.nombre);
});

console.log(personas);

sort() permite definir múltiples criterios. localeCompare() compara cadenas respetando reglas lingüísticas. Recordá que sort() modifica el array original.

4. Crear una búsqueda con varios filtros

Enunciado: Filtrar productos por texto, categoría y precio máximo.

const productos = [
    { nombre: "Mouse gamer", categoria: "tecnologia", precio: 40 },
    { nombre: "Teclado", categoria: "tecnologia", precio: 70 },
    { nombre: "Silla", categoria: "muebles", precio: 120 }
];

const texto = "mouse";
const categoria = "tecnologia";
const precioMaximo = 100;

const resultado = productos.filter(function (producto) {
    return producto.nombre.toLowerCase().includes(texto.toLowerCase())
        && producto.categoria === categoria
        && producto.precio <= precioMaximo;
});

console.log(resultado);

filter() crea un nuevo array con los elementos que cumplen todas las condiciones. Este patrón aparece mucho en buscadores y paneles administrativos.

5. Implementar paginación de un array

Enunciado: Obtener una página concreta de resultados.

function paginar(elementos, pagina, porPagina) {
    const inicio = (pagina - 1) * porPagina;
    const fin = inicio + porPagina;

    return elementos.slice(inicio, fin);
}

const datos = [1,2,3,4,5,6,7,8,9,10];

console.log(paginar(datos, 2, 3));

slice() devuelve una porción del array sin modificar el original. Para la página 2 con 3 elementos por página devuelve los elementos 4, 5 y 6.

6. Crear un carrito de compras

Enunciado: Calcular el total de un carrito con cantidades y precios.

const carrito = [
    { producto: "Mouse", precio: 25, cantidad: 2 },
    { producto: "Teclado", precio: 60, cantidad: 1 }
];

const total = carrito.reduce(function (acumulador, item) {
    return acumulador + item.precio * item.cantidad;
}, 0);

console.log(total);

El acumulador suma el subtotal de cada producto. Este patrón se usa constantemente en e-commerce y sistemas de facturación.

7. Calcular subtotal, descuento e impuestos

Enunciado: Aplicar un descuento y luego calcular impuestos.

const subtotal = 1000;
const descuentoPorcentaje = 10;
const impuestoPorcentaje = 21;

const descuento = subtotal * descuentoPorcentaje / 100;
const base = subtotal - descuento;
const impuesto = base * impuestoPorcentaje / 100;
const total = base + impuesto;

console.log({ subtotal, descuento, impuesto, total });

Separar cada etapa en variables hace que el cálculo sea más legible y fácil de auditar.

8. Transformar datos anidados de una API

Enunciado: Convertir una estructura anidada en una lista más simple.

const respuesta = {
    usuarios: [
        {
            id: 1,
            perfil: {
                nombre: "Ana",
                ciudad: "Santa Rosa"
            }
        },
        {
            id: 2,
            perfil: {
                nombre: "Luis",
                ciudad: "Córdoba"
            }
        }
    ]
};

const usuarios = respuesta.usuarios.map(function (usuario) {
    return {
        id: usuario.id,
        nombre: usuario.perfil.nombre,
        ciudad: usuario.perfil.ciudad
    };
});

console.log(usuarios);

map() es ideal para adaptar respuestas de APIs a la estructura que necesita nuestra interfaz.

9. Eliminar objetos duplicados por id

Enunciado: Conservar un solo objeto por cada id.

const usuarios = [
    { id: 1, nombre: "Ana" },
    { id: 2, nombre: "Luis" },
    { id: 1, nombre: "Ana duplicada" }
];

const mapa = new Map();

for (const usuario of usuarios) {
    mapa.set(usuario.id, usuario);
}

const unicos = [...mapa.values()];

console.log(unicos);

Map permite usar el id como clave. Cuando se repite una clave, el valor anterior se reemplaza por el nuevo.

10. Comparar dos arrays y detectar cambios

Enunciado: Obtener elementos agregados y eliminados entre dos listas.

const anterior = [1, 2, 3, 4];
const actual = [2, 3, 4, 5];

const agregados = actual.filter(function (id) {
    return !anterior.includes(id);
});

const eliminados = anterior.filter(function (id) {
    return !actual.includes(id);
});

console.log({ agregados, eliminados });

Comparar listas de esta forma es útil cuando necesitamos detectar cambios entre un estado anterior y uno nuevo.

11. Crear una función debounce()

Enunciado: Evitar ejecutar una función en cada pulsación y esperar a que el usuario deje de escribir.

function debounce(funcion, demora) {
    let temporizador;

    return function (...args) {
        clearTimeout(temporizador);

        temporizador = setTimeout(function () {
            funcion(...args);
        }, demora);
    };
}

const buscar = debounce(function (texto) {
    console.log("Buscando:", texto);
}, 500);

buscar("java");
buscar("javascript");

debounce() reinicia el temporizador cada vez que se llama. Es muy útil en buscadores, autocompletados y validaciones mientras el usuario escribe.

12. Crear una función throttle()

Enunciado: Limitar cuántas veces puede ejecutarse una función dentro de un intervalo.

function throttle(funcion, intervalo) {
    let habilitado = true;

    return function (...args) {
        if (!habilitado) {
            return;
        }

        habilitado = false;
        funcion(...args);

        setTimeout(function () {
            habilitado = true;
        }, intervalo);
    };
}

const informarScroll = throttle(function () {
    console.log("Scroll detectado");
}, 1000);

throttle() permite una ejecución cada cierto tiempo. Suele utilizarse con eventos frecuentes como scroll o resize.

13. Ejecutar tareas asíncronas en secuencia

Enunciado: Ejecutar varias operaciones respetando un orden.

function esperar(ms) {
    return new Promise(function (resolve) {
        setTimeout(resolve, ms);
    });
}

async function ejecutar() {
    console.log("Paso 1");
    await esperar(500);

    console.log("Paso 2");
    await esperar(500);

    console.log("Paso 3");
}

ejecutar();

await pausa la función async hasta que la Promise termina. Esto permite expresar procesos secuenciales de forma clara.

14. Ejecutar varias Promises en paralelo

Enunciado: Esperar varias tareas independientes al mismo tiempo.

function tarea(nombre, tiempo) {
    return new Promise(function (resolve) {
        setTimeout(function () {
            resolve(nombre);
        }, tiempo);
    });
}

async function ejecutar() {
    const resultados = await Promise.all([
        tarea("A", 500),
        tarea("B", 300),
        tarea("C", 700)
    ]);

    console.log(resultados);
}

ejecutar();

Promise.all() espera que todas las Promises terminen y devuelve sus resultados en el mismo orden en que fueron enviadas.

API utilizada en los ejercicios

En los ejemplos que utilizan fetch() vamos a trabajar con JSONPlaceholder, una API REST gratuita pensada para pruebas, aprendizaje y prototipos.

La API proporciona datos ficticios de usuarios, publicaciones, comentarios, tareas y otros recursos. Esto permite practicar peticiones HTTP, Promises, async/await y procesamiento de JSON sin necesitar un backend propio.

En estos ejercicios usamos principalmente el recurso de usuarios:

https://jsonplaceholder.typicode.com/users

Los usuarios devueltos son datos de prueba: no corresponden a personas reales ni a información de producción.

15. Manejar errores con async/await

Enunciado: Capturar errores de una operación asíncrona.

async function cargarDatos() {
    try {
        const respuesta = await fetch("https://jsonplaceholder.typicode.com/users");

        if (!respuesta.ok) {
            throw new Error("Error HTTP: " + respuesta.status);
        }

        const datos = await respuesta.json();

        console.log(datos);
    } catch (error) {
        console.log("No se pudieron cargar los datos:", error.message);
    }
}

cargarDatos();

try/catch permite capturar errores. También conviene revisar response.ok, porque fetch() no rechaza automáticamente todos los errores HTTP.

16. Consumir una API y filtrar resultados

Enunciado: Descargar usuarios y conservar sólo aquellos que cumplan una condición.

async function cargarUsuarios() {
    const respuesta = await fetch("https://jsonplaceholder.typicode.com/users");
    const usuarios = await respuesta.json();

    const filtrados = usuarios.filter(function (usuario) {
        return usuario.name.toLowerCase().includes("a");
    });

    console.log(filtrados);
}

cargarUsuarios();

Este ejercicio combina fetch(), JSON y filter(), una secuencia muy habitual en interfaces que consumen APIs.

17. Reintentar una petición fallida

Enunciado: Crear una función que intente varias veces antes de devolver un error.

async function fetchConReintentos(url, intentos = 3) {
    let ultimoError;

    for (let i = 1; i <= intentos; i++) {
        try {
            const respuesta = await fetch(url);

            if (!respuesta.ok) {
                throw new Error("HTTP " + respuesta.status);
            }

            return await respuesta.json();
        } catch (error) {
            ultimoError = error;
        }
    }

    throw ultimoError;
}

fetchConReintentos("https://jsonplaceholder.typicode.com/users")
    .then(function (datos) {
        console.log(datos);
    })
    .catch(function (error) {
        console.log(error.message);
    });

Los reintentos pueden ser útiles ante fallos temporales de red. En sistemas reales suele agregarse una espera progresiva entre intentos.

18. Guardar objetos complejos en localStorage

Enunciado: Persistir un objeto convirtiéndolo a JSON.

const configuracion = {
    tema: "oscuro",
    idioma: "es",
    filtros: ["activos", "recientes"]
};

localStorage.setItem(
    "configuracion",
    JSON.stringify(configuracion)
);

const recuperada = JSON.parse(
    localStorage.getItem("configuracion")
);

console.log(recuperada);

localStorage guarda cadenas. Por eso usamos JSON.stringify() al guardar y JSON.parse() al recuperar.

19. Crear un sistema de eventos personalizado

Enunciado: Implementar métodos on, emit y off.

class EventBus {
    constructor() {
        this.eventos = {};
    }

    on(nombre, callback) {
        if (!this.eventos[nombre]) {
            this.eventos[nombre] = [];
        }

        this.eventos[nombre].push(callback);
    }

    emit(nombre, datos) {
        const callbacks = this.eventos[nombre] || [];

        for (const callback of callbacks) {
            callback(datos);
        }
    }

    off(nombre, callback) {
        const callbacks = this.eventos[nombre] || [];

        this.eventos[nombre] = callbacks.filter(function (item) {
            return item !== callback;
        });
    }
}

const bus = new EventBus();

function saludar(nombre) {
    console.log("Hola", nombre);
}

bus.on("saludo", saludar);
bus.emit("saludo", "Ana");
bus.off("saludo", saludar);

Este patrón desacopla componentes: quien emite un evento no necesita conocer directamente a quien lo escucha.

20. Mini proyecto: buscador de usuarios desde una API

Enunciado: Crear una búsqueda que cargue usuarios, filtre por texto y maneje estados de carga y error.

const estado = {
    usuarios: [],
    filtro: ""
};

async function cargarUsuarios() {
    try {
        console.log("Cargando...");

        const respuesta = await fetch("https://jsonplaceholder.typicode.com/users");

        if (!respuesta.ok) {
            throw new Error("No se pudieron cargar los usuarios");
        }

        estado.usuarios = await respuesta.json();

        renderizar();
    } catch (error) {
        console.log(error.message);
    }
}

function obtenerFiltrados() {
    const texto = estado.filtro.toLowerCase();

    return estado.usuarios.filter(function (usuario) {
        return usuario.name.toLowerCase().includes(texto)
            || usuario.email.toLowerCase().includes(texto);
    });
}

function renderizar() {
    const usuarios = obtenerFiltrados();

    console.log(usuarios);
}

function buscar(texto) {
    estado.filtro = texto;
    renderizar();
}

cargarUsuarios();

El ejemplo separa estado, carga de datos, filtrado y renderizado. Esa separación facilita escalar luego hacia una interfaz DOM completa.

Qué conceptos avanzados practicamos

Estos ejercicios trabajan transformación de datos, composición de filtros, estructuras Map, funciones de orden superior, asincronía, Promises, APIs, persistencia local y patrones para controlar eventos frecuentes.

En varios casos existe más de una solución válida. El objetivo no es memorizar código, sino reconocer qué herramienta resulta apropiada para cada problema y entender si una operación modifica los datos originales o devuelve una nueva estructura.

Seguí aprendiendo JavaScript


¿Te sirvió esta guía? ☕

Si este contenido te ayudó a aprender JavaScript, podés apoyar a Club Programador. Cada aporte ayuda a mantener el sitio online y a seguir publicando material gratuito.

🇦🇷 Apoyar desde Argentina

☕ Invitame un café con Mercado Pago

🌎 Apoyar desde otro país

☕ Apoyar con PayPal