Zápočtový program (Prolog)

Miroslav Spousta

1. Zadání

Zadání č. 340 -- hra NIM. Z daných N hromádek zápalek odebírají dva soupeři.
Ukončení a povolené tahy: Úkolem je odehrát optimální strategii pro tuto hru.

2. Rozbor

2.1. Zavedení pojmů

2.2. Rozbor hry

Hra začíná na smluvené pozici. Soupeři střídavě odebírají povolené počty zápalek. Kdo nemůže táhnout, prohrál. Hra je konečná, protože se každým tahem snižuje počet zápalek na hromádkách. Každá pozice hry může být ohodnocena jako: Musí být každá pozice jedním z těchto případů? Ano, první podmínka je totiž negací druhé a opačně. (Když existuje tah na silně prohrávající pozici tak neplatí, že všechny tahy vedou na pozice slabě vyhrávající.)

2.3. Jak to naprogramovat? -- Popis algoritmu

Úkolem je odehrát optimální strategii. To znamená, že potřebujeme (1) najít ohodnocení zadané pozice a (2) doporučit (co možná nejlepší) tah. Jak toho dosáhnout?

První úkol splníme tak, že budeme postupně zkoumat všechny pozice (od 0, 0, ..., 0) až po zadanou pozici a získané výsledky si budeme ukládat. Pro každou zkoumanou pozici zjistíme všechny z ní přípustné tahy. Ohodnocení cílových pozic těchto tahů již známe (neboť provedením tahu se sníží počet zápalek na jedné z hromádek). To nám stačí k určení hledaného ohodnocení pozice jako (silně) prohrávající nebo (slabě) vyhrávající. Neexistuje-li z některé pozice žádný tah, označíme ji jako prohrávající.

Druhý úkol je pak již jednoduchý. Podíváme se do uložených výsledků,
zjistíme ohodnocení pozice a vrátíme:

3. Realizace programu

Program je napsán v programovacím jazyku Prolog.

3.1. Reprezentace dat v programu

3.1.1 Reprezentace jedné pozice

Pro reprezentaci pozice jsem použil seznam, zejména kvůli snadné implementaci a možnosti adaptování na libovolný počet hromádek.

3.1.2. Reprezentace pozic s již známým ohodnocením

Je zřejmé, že v průběhu výpočtu si nemusím pamatovat ohodnocení každé pozice, ale jen seznam buď prohrávajících nebo seznam vyhrávajících pozic. Zvolil jsem první variantu, ale myslím, že stejně dobře jsem mohl zvolit druhou.

Samotné uložení pozic bylo možné realizovat dvěma způsoby:

Netestoval jsem rychlost obou variant, ale předpokládám, že ukládání do databáze je časově náročnější než práce se seznamem. Proto jsem zvolil druhou variantu.

Prohrávající pozice jsou uloženy v databázi jako predikát nim_lostdb/2:

nim_lostdb(List1, List2)
kde List1 je největší prozkoumaná pozice a List2 seznam těch pozic, které
jsou prohrávající od 0, 0, ..., 0 až po List1.

3.1.3. Implicitní pozice

Implicitní pozice (viz. 3.3.1.) se ukládá do databáze prologu jako predikát nim_defpos/1:
nim_defpos(List)
kde List je pozice.

3.1.4. Reprezentace možných tahů

Je řešena jako externí program v jazyku Prolog. Každý tah je určen predikátem move/2:
move(+Pos1, -Pos2)
který musí uspět pravě když existuje tah z pozice Pos1 a vrátit cíl tahu -- pozici Pos2. Na žádnou z hromádek nesmí provedením tahu přibýt zápalka a Pos2 musí obsahovat méně zápalek než Pos1. Ve stejném externím programu jako tahy musí být definován také predikát heaps/1 (parametr je celé číslo) určující počet hromádek se kterým pracuje predikát move/2.

3.2. Popis programu

Celý program je možné rozdělit na několik částí:
  1. "seznamové" podprogramy -- generování, porovnávání, maximum ze dvou seznamů...
  2. podprogramy na generování a testování pozic (jsou-li prohrávající)
  3. user-interface podprogramy -- čtení řádky textu, převod string -> číslo, výpis nápovědy, seznamu prohrávajících pozic, volání hlavních klauzulí...
  4. hlavní klauzule generate/2 a explore/3 -- generate vytvoří databázi prohrávajících pozic, explore vrátí ohodnocení pozice a doporučený tah.
  5. klauzule nim/1 -- hlavní klauzule zajišťující načtení databáze tahů a interakci s uživatelem.

3.3. Popis rozhraní

3.3.1. Uživatelské rozhraní

Po načtení souboru (consult('nim.pl').) se interaktivní rozhraní spustí jako volání klauzule "nim.".
Program požádá o zadání názvu souboru s tahy:

enter filename with move specs [moves]:

(implicitně je nastaveno "moves.pl", při akceptování inplicitního nastavení stačí vždy stisknout enter). Potom přečte databázi tahů, vypíše nakolika hromádkách se hraje a požádá o zadání požadované pozice:

moves compiled, 0.01 sec, 1,052 bytes.
loading moves.. done.
number of heaps: 2

enter current heaps (or type "help") [5, 5]:

(implicitně se nastaví 5, 5, ..., 5). Nyní můžeme napsat požadovanou pozici (např. 7, 9) a stisknout enter. Program odpoví:

[7, 9] is losing position, possible move: [3, 9].

enter current heaps (or type "help") [3, 9]:

Což znamená, že pozice "7, 9" je prohrávající a doporučí nám jeden z možných tahů (odebrat 4 zápalky z první hromádky, tedy hrát na pozici "3, 9". Implicitní hodnota se nastaví také na tuto pozici. Dalším stiskem enter dostaneme:

[3, 9] is winning position, possible move: [1, 9].

a tak dále až do pozice ze které již neexistuje žádný tah:

[0, 0] is losing position, no moves found.

Možnosti vstupu (na výzvu "enter current heaps..."):

Oddělovače čísel tvoří mezera, čárka, znaky '[' a ']', takže můžeme též napsat např: "[[3]4,,13[" ve významu stejném jako "3, 4, 13".

3.3.2. Programový interface

Hlavní klauzule lze volat samostatně, pokud jsou definovány tahy (predikátem move/1). Toto lze splnit například přečtením souboru s tahy (predikátem consult(file).)

Chceme-li zjistit ohodnocení některé pozice, je třeba:

  1. vygenerovat seznam prohrávajících pozic klauzulí generate/2:
    generate(+Pos, +Heaps)
    kde Pos je požadovaná pozice a Heaps je počet hromádek (musí souhlasit s počtem hromádek pro který jsou definované tahy (move/2); seznam se generuje pouze pokud již není uložen v databázi.
  2. zjistit ohodnocení a doporučený tah: klauzulí explore/3:
    explore(+Pos, -Newpos, -Code)
    kde Pos je požadovaná pozice, Newpos je doporučený tah a Code =

4. Dodatky

Program je napsán v jazyku Prolog. Odlaďen a vyzkoušen byl v implementaci SWI-Prolog (Version 3.2.8).
Program používá tyto standartní predikáty: get0/1, write/1, nl/0, fail/0, retractall/1, assertz/1, repeat/0, consult/1 a porovnávání celých čísel.