Mostrar Mensajes

Esta sección te permite ver todos los posts escritos por este usuario. Ten en cuenta que sólo puedes ver los posts escritos en zonas a las que tienes acceso en este momento.


Mensajes - marianolloret

Páginas: [1] 2 Siguiente
1
Juegos / Re: Kings of the Beach
« en: Julio 22, 2026, 16:53:43 pm »
Estoy pensando en si sería posible desensamblar el juego mediante una herramienta de IA ... que generara un fichero en lenguaje C con el código contextualizado y bien documentado.

2
Juegos / Re: Kings of the Beach
« en: Julio 22, 2026, 16:26:21 pm »
Como cada verano, he vuelto a engancharme a este juego. Esta vez, aprovechando el auge de la inteligencia artificial, me pregunté si podría usarla para modificarlo. Después de unas cuantas pruebas, puedo decir que algo he conseguido.

Le pregunté a Gemini si existía algún sitio web en el que se explicara técnicamente el funcionamiento interno del juego, y me recomendó la siguiente página:

https://moddingwiki.shikadi.net/wiki/Kings_of_the_Beach_PAK_Format

En ella se documenta con detalle la estructura interna de los archivos .PAK, explicando el sistema de compresión RLE utilizado por el juego, el formato de las cabeceras y la organización de los datos una vez descomprimidos. Además, describe cómo se representan las imágenes en formato EGA de 4 bits por planos, el proceso necesario para reconstruirlas correctamente y las particularidades de algunos archivos parciales utilizados para superponer gráficos sobre otros. La documentación también incluye ejemplos de la estructura binaria y herramientas compatibles para visualizar, convertir y modificar estos archivos, convirtiéndose en una referencia imprescindible para cualquiera que quiera analizar o modificar los recursos gráficos de Kings of the Beach.

Pues bien, con esto le pedí a Claude si podía crear un script en Python para convertir los ficheros de PAK a PNG y para mi sorpresa lo hizo en menos de un minuto:

#!/usr/bin/env python3
"""
kotb_pak_converter.py
======================

Convierte entre el formato de imagen .PAK del juego "Kings of the Beach"
(EGA, 4 bits, planar, comprimido con RLE de tipo "code-based") y PNG.

Uso:
    # PAK -> PNG
    python3 kotb_pak_converter.py pak2png ENTRADA.PAK SALIDA.png

    # PNG -> PAK
    python3 kotb_pak_converter.py png2pak ENTRADA.png SALIDA.PAK [--height N] [--no-partial]

    # Autodetectar dirección según la extensión de los ficheros
    python3 kotb_pak_converter.py convert ENTRADA SALIDA

Notas:
- La paleta usada es la paleta EGA estándar de 16 colores.
- Al convertir PNG -> PAK, los colores de la imagen se ajustan (nearest
  color) a la paleta EGA de 16 colores más cercana.
- El ancho de la imagen debe ser múltiplo de 8 (requisito del formato).
- Si la imagen PNG tiene un bloque de líneas totalmente negras (índice de
  color 0) al final, se codifica como "archivo parcial" (igual que hacen
  los ficheros de canchas del juego), salvo que se use --no-partial.
"""

import argparse
import struct
import sys
from pathlib import Path

try:
    from PIL import Image
except ImportError:
    sys.exit("Este script necesita Pillow. Instálalo con: pip install Pillow")


# ---------------------------------------------------------------------------
# Paleta EGA estándar (16 colores)
# ---------------------------------------------------------------------------
EGA_PALETTE = [
    (0, 0, 0), (0, 0, 170), (0, 170, 0), (0, 170, 170),
    (170, 0, 0), (170, 0, 170), (170, 85, 0), (170, 170, 170),
    (85, 85, 85), (85, 85, 255), (85, 255, 85), (85, 255, 255),
    (255, 85, 85), (255, 85, 255), (255, 255, 85), (255, 255, 255),
]


# ---------------------------------------------------------------------------
# RLE "code-based" (bit alto = copiar literal / bit alto apagado = repetir)
# ---------------------------------------------------------------------------
def rle_decompress(comp: bytes) -> bytes:
    out = bytearray()
    i = 0
    n = len(comp)
    while i < n:
        code = comp
        i += 1
        if code & 0x80:
            amt = code & 0x7F
            out.extend(comp[i:i + amt])
            i += amt
        else:
            amt = code
            if amt == 0:
                # Código sin uso práctico (repetir 0 veces); lo saltamos.
                continue
            val = comp
            i += 1
            out.extend([val] * amt)
    return bytes(out)


def rle_compress(data: bytes) -> bytes:
    out = bytearray()
    literal_buf = bytearray()

    def flush_literal():
        pos = 0
        while pos < len(literal_buf):
            chunk = literal_buf[pos:pos + 127]
            out.append(0x80 | len(chunk))
            out.extend(chunk)
            pos += len(chunk)
        literal_buf.clear()

    i = 0
    n = len(data)
    while i < n:
        run = 1
        while i + run < n and data[i + run] == data and run < 127:
            run += 1
        if run >= 2:
            flush_literal()
            out.append(run)          # bit alto apagado => Repeat
            out.append(data)
            i += run
        else:
            literal_buf.append(data)
            i += 1
    flush_literal()
    return bytes(out)


# ---------------------------------------------------------------------------
# PAK -> PNG
# ---------------------------------------------------------------------------
def pak_to_png(pak_path: str, png_path: str) -> None:
    raw = Path(pak_path).read_bytes()
    if len(raw) < 4:
        raise ValueError("Fichero PAK demasiado pequeño / inválido")

    declen = raw[-2] | (raw[-1] << 8)
    comp = raw[:-2]

    decompressed = rle_decompress(comp)
    if len(decompressed) != declen:
        print(f"Aviso: longitud descomprimida ({len(decompressed)}) "
              f"no coincide con la esperada ({declen})", file=sys.stderr)

    bytewidth = decompressed[0]
    height = decompressed[1]
    imgdata = decompressed[2:]

    width = bytewidth * 8
    lines_available = len(imgdata) // (bytewidth * 4)
    if lines_available < height:
        print(f"Info: fichero parcial -> {lines_available} de {height} líneas "
              f"presentes (el resto se rellena en negro)", file=sys.stderr)

    img = Image.new("RGB", (width, height), EGA_PALETTE[0])
    pixels = img.load()

    for line in range(lines_available):
        base = line * bytewidth * 4
        planes = [
            imgdata[base + p * bytewidth: base + (p + 1) * bytewidth]
            for p in range(4)
        ]
        for xb in range(bytewidth):
            b0, b1, b2, b3 = (planes[0][xb], planes[1][xb],
                              planes[2][xb], planes[3][xb])
            for bit in range(8):
                shift = 7 - bit
                color_index = (
                    ((b0 >> shift) & 1)
                    | (((b1 >> shift) & 1) << 1)
                    | (((b2 >> shift) & 1) << 2)
                    | (((b3 >> shift) & 1) << 3)
                )
                x = xb * 8 + bit
                pixels[x, line] = EGA_PALETTE[color_index]

    img.save(png_path)
    print(f"OK: {pak_path} -> {png_path} ({width}x{height}, "
          f"{lines_available} líneas de datos reales)")


# ---------------------------------------------------------------------------
# PNG -> PAK
# ---------------------------------------------------------------------------
def _nearest_ega_index(rgb):
    r, g, b = rgb[:3]
    best_i, best_d = 0, None
    for i, (pr, pg, pb) in enumerate(EGA_PALETTE):
        d = (r - pr) ** 2 + (g - pg) ** 2 + (b - pb) ** 2
        if best_d is None or d < best_d:
            best_d, best_i = d, i
    return best_i


def png_to_pak(png_path: str, pak_path: str, height: int = None,
                allow_partial: bool = True) -> None:
    img = Image.open(png_path).convert("RGB")
    width, img_height = img.size

    if width % 8 != 0:
        raise ValueError(f"El ancho ({width}) debe ser múltiplo de 8")
    if width > 320 or img_height > 200:
        raise ValueError("El tamaño máximo soportado es 320x200")

    bytewidth = width // 8
    full_height = height if height is not None else img_height
    if img_height > full_height:
        raise ValueError("La imagen es más alta que la 'Height' indicada")

    pixels = img.load()

    # Índice de color EGA (0-15) por cada píxel
    indices = [
        [_nearest_ega_index(pixels[x, y]) for x in range(width)]
        for y in range(img_height)
    ]
    # Rellenamos hasta full_height con líneas negras (índice 0) si hiciera falta
    for _ in range(full_height - img_height):
        indices.append([0] * width)

    # Detectar líneas negras finales para codificar como fichero "parcial"
    lines_to_store = full_height
    if allow_partial:
        while lines_to_store > 0 and all(v == 0 for v in indices[lines_to_store - 1]):
            lines_to_store -= 1
        if lines_to_store == 0:
            lines_to_store = full_height  # imagen totalmente negra: guardar completa

    imgdata = bytearray()
    for line in range(lines_to_store):
        row = indices[line]
        planes = [bytearray(bytewidth) for _ in range(4)]
        for xb in range(bytewidth):
            for bit in range(8):
                x = xb * 8 + bit
                val = row

                shift = 7 - bit
                for p in range(4):
                    if (val >> p) & 1:
                        planes[p][xb] |= (1 << shift)
        for p in range(4):
            imgdata.extend(planes[p])

    decompressed = bytes([bytewidth, full_height]) + bytes(imgdata)
    declen = len(decompressed)
    if declen > 0xFFFF:
        raise ValueError("Los datos descomprimidos exceden 65535 bytes")

    compressed = rle_compress(decompressed)

    out = compressed + struct.pack("<H", declen)
    Path(pak_path).write_bytes(out)

    partial_note = (f" (parcial: {lines_to_store}/{full_height} líneas)"
                     if lines_to_store < full_height else "")
    print(f"OK: {png_path} -> {pak_path} ({width}x{full_height}){partial_note}")


# ---------------------------------------------------------------------------
# CLI
# ---------------------------------------------------------------------------
def main():
    parser = argparse.ArgumentParser(description=__doc__,
                                      formatter_class=argparse.RawDescriptionHelpFormatter)
    sub = parser.add_subparsers(dest="mode", required=True)

    p1 = sub.add_parser("pak2png", help="Convierte un fichero .PAK a .png")
    p1.add_argument("input")
    p1.add_argument("output")

    p2 = sub.add_parser("png2pak", help="Convierte un fichero .png a .PAK")
    p2.add_argument("input")
    p2.add_argument("output")
    p2.add_argument("--height", type=int, default=None,
                     help="Alto declarado en la cabecera (por defecto, el alto real del PNG)")
    p2.add_argument("--no-partial", action="store_true",
                     help="Fuerza la codificación completa, sin recortar líneas negras finales")

    p3 = sub.add_parser("convert", help="Autodetecta la dirección según la extensión")
    p3.add_argument("input")
    p3.add_argument("output")
    p3.add_argument("--height", type=int, default=None)
    p3.add_argument("--no-partial", action="store_true")

    args = parser.parse_args()

    if args.mode == "pak2png":
        pak_to_png(args.input, args.output)
    elif args.mode == "png2pak":
        png_to_pak(args.input, args.output, height=args.height,
                   allow_partial=not args.no_partial)
    elif args.mode == "convert":
        in_ext = Path(args.input).suffix.lower()
        out_ext = Path(args.output).suffix.lower()
        if in_ext == ".pak" and out_ext == ".png":
            pak_to_png(args.input, args.output)
        elif in_ext == ".png" and out_ext == ".pak":
            png_to_pak(args.input, args.output, height=args.height,
                       allow_partial=not args.no_partial)
        else:
            sys.exit("No se pudo determinar la dirección de conversión "
                      "a partir de las extensiones de fichero.")


if __name__ == "__main__":
    main()

ahora se ha abierto la posibilidad de modificar los gráficos del juego.

PD: Para mi sorpresa, Claude me sugirió que si le indicaba los canvios que quería hacer en el fichero PAK intentaría realizarlos él sin la necesidad de convertir el fichero a PNG y luego de nuevo a PAK  :huh:

3
Recopilaciones / Re: Revista - OK PC
« en: Septiembre 04, 2022, 20:01:07 pm »
¡Muchas gracias!

4
Juegos / Re: Kings of the Beach
« en: Agosto 21, 2022, 12:08:12 pm »
Gracias!

5
Juegos / Re: Kings of the Beach
« en: Agosto 21, 2022, 00:27:38 am »
Buenas noches,

Os dejo la imagen de diskette que he encontrado, la ruleta de códigos interactiva y la medicina.
Aceptará cualquier cadena introducida.

En este caso, son 3 los archivos medicados, ya que según los gráficos seleccionados, carga un archivo u otro.

Probado en DOSBox 0.74-3 sin finalizar.
:disco: 1x Diskette 3.5" DD .IMG :en: (210 KB)
:zip: Medicina :en: (61 KB)
:zip: Ruleta Códigos .HTM (2,62 MB)

Salu2

¿Cómo puedo conseguir una ruleta de código como esa?

Por ejemplo, pasar la de Bard's Tale 3 de la web (https://www.oldgames.sk/codewheel/bard-s-tale-3-thief-of-fate) a un fichero local html que pueda consultar sin conexión.

6
Juegos / Re: Asterix En La India
« en: Agosto 18, 2022, 16:51:28 pm »
Hola!

El juego funciona bien en el DOSBox, cuando llegas a la pantalla de Obelix comiendo el jabali hay que esperar unos segundos y aparece una flecha que la movemos con el teclado y seleccionamos a alguno de los personajes que aparece en pantalla (por ejemplo Asterix).

Que estúpido soy ... no me había dado cuenta, muchas gracias!!!

7
Juegos / Re: Asterix En La India
« en: Agosto 17, 2022, 15:22:08 pm »
Buenas,

¿Alguien ha probado el juego? A mi con DOSBox se me acaba colgando en la tercera pantalla de presentación (con la animación de comer un jabalí).

¿Qué hago mal?

8
Juegos / Re: Pack Opera 25 (1994)
« en: Agosto 16, 2022, 10:59:56 am »
El enlace de onedrive sigue siendo válido y están los archivos

Tienes razón, estaba viendo el hilo con el navegador Firefox y no se construye bien, con Chrome sí y he podido encontrar el enlace.

Gracias por vuestra ayuda!

9
Juegos / Re: Pack Opera 25 (1994)
« en: Agosto 11, 2022, 22:40:31 pm »
Creo que los enlaces estan rotos, ¿alguien lo pude volver a colgar? El CD y el manual si es posible.

Gracias por vuestra atención.

10
Juegos / Re: La Abadía del Crimen
« en: Abril 06, 2022, 23:53:01 pm »
De los remakes que comentas:


Hoy en día, el segundo es más interesante que el primero.

11
Juegos / Re:Microsoft Flight Simulator 4.0
« en: Agosto 13, 2021, 10:32:24 am »
Recuerdo que hace mucho tiempo un conocido mío tuvo este juego, el original, y estava compuesto por dos dicos, el de programa y el de escenarios.

¿Estava versión de un disco es una revisión/redición?

12
Juegos / Re:Kings of the Beach
« en: Julio 29, 2021, 10:12:19 am »
Encontre una guía del juego:

https://gamefaqs.gamespot.com/pc/926297-kings-of-the-beach/faqs/35263


contacté con el autor y me ha indicado que no existe una combinación de teclas para el dink, la mayoría de las veces se realiza al azar, pero siempre se realiza si ambos oponentes están cerca de ti debajo de la red.

También he descubierto que el 'dink' en la versión de NES se denomina 'The Feint' y es muy facil de hacer.

13
Juegos / Re:Kings of the Beach
« en: Julio 22, 2021, 17:02:19 pm »
Aquí pongo dos fragmentos del vídeo, para ver exáctamente a que me refiero:

spike: https://youtu.be/X-FnQLVO4IU?t=2312

dink: https://youtu.be/X-FnQLVO4IU?t=3808

¿alguien sabe como hacer uno u otro?

14
Peticiones de abandonware / Re:Imágenes originales disquete
« en: Julio 04, 2021, 19:58:51 pm »
Los juegos empaquetados en un zip suelen estar alterados: preconfigurados y con registros de tiempos o records.

Estoy de acuerdo contigo, lo mejor con diferencia son los ficheros con las imágenes de los discos. Es la manera de poder realizar una instalación con los ficheros originales.

Respecto el juego Lemmings, las imágenes de los discos no funcionan, pero hay un versión cd que incluye los dos primeros juegos que creo que permite instalarlo.

Rick Dangerous 1 y 2 creo que los podrías encontrar en "The Good Old Days IBM PC Floppy Collection" o "Floppy Disks of Software" de Internet Archive, de todos modos creo que los tengo.


15
Juegos / Re:Kings of the Beach
« en: Julio 04, 2021, 17:02:21 pm »
Una duda sobre un aspecto del juego que no consigo resolver.

Me he dado cuenta que cuando estás atacando, si optas por hacer un 'spike' (tecla L), en ocasiones en lugar de rematar (spike) le das un toque a la pelota (dink) que despista al rival.

En el manual (https://www.retrogames.cz/manualy/DOS/Kings_of_the_Beach_-_DOS_-_Manual.pdf), en la sección "Glossary", dice sobre 'Dink' "A technique where the hitter goes up as if he'll spike, but taps the ball over the net instead.", pero no especifica como realitzar el 'dink'.

En la Wikipedia se puede leer: "The only 'advanced' moves in the game are the ability to dink or perform a one-handed Kong block (Stoklos's trademark).".

¿Alguien sabe cuando realiza un 'spike' y cuando un 'dink'?

Páginas: [1] 2 Siguiente