Warning
This page is located in archive. Go to the latest version of this course pages. Go the latest version of this page.

Cvičení 9 - Semestrální práce - GEMBLO

Semestrální práce spočívá ve vytvoření funkčního hráče (programu) pro Gemblo . Gemblo je desková hra pro 2-6 hráčů, která se hraje na hexagonálním gridu s kameny, které jsou podobné tetrisovým kostičkám. Začíná se z prázdné hrací desky, hráči postupně umisťují svoje kameny. Vyhrává hráč, který svojí barvou obsadí nejvíce buněk. Obtížnost hry spočívá v pravidlech pro umisťování kamenů: kameny stejné barvy musí mít mezi sebou minimální vzdálenost právě jedné hrany, kameny různých barev se mohou dotýkat.

Historie a balíček

Balíček obsahuje základní třídy a programy pro vyzkoušení Gemblo na vašem počítači:

  • base.py — základní třída, hráči jsou potomky této třídy
  • player.py — váš hráč
  • game.py — test vašeho hráče, kdy hraje sám proti sobě
  • stones.txt - příklad hracích kamenů (načítání přes base.loadStones)
  • Soubory mějte ve stejném adresáři

V případě, že nastanou změny v zadání nebo bude např. upraveno/rozšířeno herní rozhraní, budou změny uvedeny v této sekci.

Turnaj

FAQ

  • V jakém pořadí se dávají kameny na hrací desku?
    • Hráč umisťuje kameny na hrací desku v jakémkolik pořadí (ne nutně v tom, jak je má v poli self.stones).
    • Zároveň platí, že hráči mohou dostat kameny zamíchané (aby se zesložitilo předvídání, jaký tah udělá protihráč)
  • Co když zapíši do hrací desky 'za protihráče', nebo co když smažu jeho kameny?
    • Co si zapisujete do svého self.board není důležité, přístup do protihráčova self.board ani nemáte.
    • Podstatné je, co vrací metoda move(). Brute po každém tahu přepíše váš self.board tak, aby oba hráči měli stejnou hrací desku.
  • Pokud je na začátku hry deska předvyplněna, je možné použít všechny kameny, nebo jen ty, které ještě nejsou v hrací desce?
    • Můžete použít všechny kameny, které dostanete (bez ohledu na to, jestli už jsou v hrací desce nebo ne)
  • Je možné používat vlákna nebo další procesy?
    • Není.
  • Je možné používat knihovnu xzy?
    • Defaultní pythoní knihovny (sys, copy, random atd) samozřejme ano.
    • Ohledně ostatních, kontaktuje Vojtu Vonaska , ověřím, jestli je váš požadavek možný.
    • Důvodem je, že turnaj/Brute běží na prostředí, ve kterém nemáme možnost instalace knihoven “na přání”
    • p.s. pro vypracování semestrálky nejsou potřeba žádné externí knihovny

Pravidla

  • V semestrálce sice vycházíme ze hry Gemblo, nicméně závazná jsou pouze pravidla uvedená na této stránce.
  • Hra je pro dva hráče (označujeme 1 a -1), začínat může kterýkoliv z nich
    • typ vašeho hráče je self.player, protihráč je pak -self.player
    • kameny, které jste zapsali na hrací desku budou tedy mít hodnotu self.player
    • kameny protihráče mají hodnotu -self.player
  • Hraje se na hexagonální desce o rozměrech nejméně 10×10 (v turnaji a na Brutovi nejspíš 13×13 nebo 15×15)
    • hrací deska je reprezentována proměnnou self.board (dictionary)
    • buňky adresujeme souřadnicemi (p,q):self.board[p][q]
  • Každá z hráčů má stejné kameny (stejný počet a stejné typy). Kameny dostávají hráči v náhodném pořadí.
    • Kameny jsou v poli self.stones
  • Hra může začít s již předvyplněnou hrací deskou (viz dále)
    • Prázdné políčko v hrací desce má hodnotu 0 (nula), pokud bude deska předvyplněná, budou tam hodnoty self.player a/nebo -self.player
    • Hra s předvyplněnou hrací deskou slouží k tomu, aby Brute mohl otestovat reakci vašeho hráče na složitější situace
  • Nechť buňka v horním levém roku je startovní pro hráče 1. Obdobně je buňka ve spodním pravém rohu startovní pro hráče -1.
    • Startovní souřadnice svého hráče získáte přes: p,q = self.getStartCoordinates(self.player)
  • Hráči se ve hře střídají. První tah se od dalších liší podle toho, jestli už je na desce něco umístněno nebo ne.
  • První tah:
    • oba hráči si zkontrolují jestli jsou jejich startovní pozice volné - p,q=self.getStartCoordinates(self.player)
      • Pokud ano, musí umístit jeden ze svých kamenů tak, aby pokryl odpovídající startovní pozici.
      • Jiný tah je v této situaci nepřípustný
    • V případě, že při prvním tahu je již startovní pozice hráče vyplněna, postupuje se podle bodu 'Další tahy'
  • Další tahy
    • Hráč vybere jeden ze svých dosud neumístěných kamenů a položí jej na hrací plochu tak, aby:
      • nejmenší vzdálenost mezi kameny stejné barvy byla právě jedna hrana
      • tj. kameny stejné barvy se nesmí dotýkat
      • ale je možné se libovolně dotýkat kamenů jiné barvy
    • každý kamen je možné použít maximálně jednou
    • kameny lze při umisťování na desku libovolně rotovat a posouvat. Je zakázáno jejich vodorovné a vertikální překlápění
    • Hráč je povinen umístit nějaký kámen, pokud taková možnost existuje
    • Pokud není možné táhnout (tj. buď už není k dispozici žádný kámen nebo žádný dostupný nelze vhodně umístit), vrátí hráč informaci, že netáhne.
    • Hra končí, pokud oba hráči po sobě nic neumístí.
    • Vyhrává ten hráč, který obarvil více buněk kameny své barvy (záleží na počtu buněk, nikoliv kamenů).
  • Doba jednoho tahu jednoho hráče je omezena na 1 s (tzv. timeout)

Jak odevzdat hráče

  • Odevzdávejte soubor player.py do úlohy SEM
  • Odevzdání jiných souborů není možné (důvod je zabránění kolizím jmen souborů/balíčků v turnaji)
  • Pokud potřebujete více tříd (nebo si naopak rozšířit třídu Board), udějete to v souboru player.py

Definice

  • Buňky v hexagonálním gridu adresujeme speciálním souřadnicovým systémem (p,q), kde p je odpovídá sloupci, 'q' označuje řádek.
  • Buňka vlevo nahoře má souřadnici (0,0), p roste směrem doprava, q roste směrem dolu

  • Následující obrázky ukazují validní umístění kamene do vzdálenosti 1 hrany (žlutě) od zeleného kamene

  • Ukázka nesprávného umístění, které je více než jedna hrana (červeně) od již položeného zeleného kamene

  • Jeden kamen je reprezentován polem [ [p1,q1], … , [pn,qn] ]
  • Počáteční pozice kamenů můžou mít libovolné souřadnice, mohou například leže mimo hrací desku, nebo být naopak vycentrované apod.

Implementace

  • Níže jsou popsány soubory base.py, player.py a game.py.
  • Tyto soubory najdete v balíčku

Hrací deska

  • Třída Board (soubor base.py) obsahuje užitečné proměnné a metody, které se vám mohou hodit při programování
  • Soubor base.py neměňte
  • Proměnné:
    • board: hrací deska, proměnná je typu dictionary
      • self.board[p][q] : buňka na souřadnici (p,q)
      • self.algorithmName : jméno vašeho algoritmu (pro turnaj), vyplňuje se ve tříde Player
      • self.size : velikost hrací desky
      • self.stones : moje hrací kameny
  • Metody:
    • inBoard(p,q) : vrací True, pokud buňka (p,q) leží uvnitř hrací desky
    • getScore(player) : vrací počet kamenů obsazených daným hráčem
      • self.getScore(self.player) - počet buněk s mými kameny
      • self.getScore(-self.player) - počet buněk obsazených protihráčem
    • distance(p1,q1,p2,q2) : vrací celočíselnou vzdálenost mezi dvěma buňkami (p1,q1) a (p2,q2)
    • saveImage(pngFile) : uloží obraz hrací desky do PNG.
    • getStartCoordinates(player) : vrátí startovní souřadnice hráče
      • p,q = getStartCoordinates(self.player) - moje startovní souřadnice
    • rotateRight(p,q) : realizuje jednu rotace souřadnice (p,q) doprava o 60 stupňů (rotace kolem bodu (0,0) )
    • rotateLeft(p,q) : realizuje jednu rotace souřadnice (p,q) doleva o 60 stupňů (rotace kolem bodu (0,0) )

Soubor base.py

import copy
import math
from PIL import Image, ImageDraw
 
 
"""
This is the base class for Gemblo game. 
 
Note that this base.py is different from base.py for HW08. Use only this file for Gemblo.
 
DO NOT MODIFY THIS FILE !!!!
 
Brute ALWAYS replaces base.py by it's own version to ensure that both players use the same base.py
 
 
For python experts: If you need to extend the Board class, make your own class (in player.py):
 
class Board2(base.Board):
    def __init__(self, plater, size, stones):
        base.Board.__init__(self, player, size, stones)
 
    def yourfunctions():
        pass
 
and update the Player class like this:
 
class Player(Board2):
    def __init__(self, player, size, stones):
        Board2.__init__(self, player, size ,stones)
        self.usedStone = [False]*len(self.stones) #all stones are free to use now 
 
 
"""
 
 
def loadStones(filename):
    f = open(filename,"r")
    stones = []
    for line in f:
        coords = list(map(int, line.rstrip().split()))
        if len(coords) > 0:
            stones.append( [] )
            for i in range(len(coords)//2):
                x = coords[2*i]
                y = coords[2*i+1]
                stones[-1].append([ x,y ] )
    return stones;
 
 
def updatePlayers(board, stones, value):
    """ fill the board by the stones with given value
        board: object of base.Board
        stones: [ [p1,q1], .... [pn,qn] ] - list of absolut coordinates of cells in the board
        value: color/value to be written to the board
    """
    try:
        for i in range(len(stones)):
            p,q = stones[i]
            board.board[p][q] = value #we write directly without checking if (p,q) is in the board
                                      #we assume that player/brute handle it
    except:
        print("Error when writing stones to the board. The player.move() returned invalid stones")
        return False
    return True
 
 
class Board:
    def __init__(self, player, size, stones):
        self.size = size
        self.board = {}
        self.stones = copy.deepcopy(stones)
        self.player = player #1 or -1
        self.algorithmName = "name of your method";
        self._playerName = "default"
 
 
        #create empty board as a dictionary
        self.b2 = {}
        for p in range(-self.size,self.size):
            for q in range(-self.size, self.size):
                if self.inBoard(p,q):
                    if not p in self.board:
                        self.board[p] = {}
                    self.board[p][q] = 0
 
                    if not q in self.b2:
                        self.b2[q] = {}
                    self.b2[q][p] = 0
 
        #this is for visualization and to synchronize colors between png/js
        self._colors = {}
        self._colors[-1] = "#fdca40" #sunglow
        self._colors[0] = "#ffffff" #white
        self._colors[1] = "#947bd3" #medium purple
        self._colors[2] = "#ff0000" #red
        self._colors[3] = "#00ff00" #green
        self._colors[4] = "#0000ff" #blue
        self._colors[5] = "#566246" #ebony
        self._colors[6] = "#a7c4c2" #opan
        self._colors[7] = "#ADACB5" #silver metalic
        self._colors[8] = "#8C705F" #liver chestnut
        self._colors[9] = "#FA7921" #pumpkin
        self._colors[10] = "#566E3D" #dark olive green
 
    def getStartCoordinates(self, player):
        if player == 1: 
            return 0,0
        else:
            return (self.size)//2, self.size-1
 
 
 
    def inBoard(self,p,q):
        """ return True if (p,q) is valid coordinate """
        return (q>= 0) and (q < self.size) and (p >= -(q//2)) and (p < (self.size - q//2))
 
 
    def rotateRight(self,p,q):
        pp = -q
        qq = p+q
        return pp,qq
 
    def rotateLeft(self, p,q):
        pp = p+q
        qq = -p
        return pp, qq
 
 
 
    def saveImage(self, filename):
        """ draw actual board to png. Empty cells are white, -1 = red, 1 = green, other values according to
            this list 
            -1 red, 0 = white, 1 = green 
        """
 
        cellRadius = 25
        cellWidth = int(cellRadius*(3**0.5))
        cellHeight = 2*cellRadius
 
        width = cellWidth*self.size + cellRadius*3
        height = cellHeight*self.size
 
        img = Image.new('RGB',(width,height),"white")
 
        draw = ImageDraw.Draw(img)
 
        lineColor = (50,50,50)
 
 
        for p in self.board:
            for q in self.board[p]:
                cx = cellRadius*(math.sqrt(3)*p + math.sqrt(3)/2*q) + cellRadius
                cy = cellRadius*(0*p + 3/2*q) + cellRadius
 
                pts = []
                for a in [30,90,150,210,270,330]:
                    nx = cx + cellRadius * math.cos(a*math.pi/180)
                    ny = cy + cellRadius * math.sin(a*math.pi/180)
                    pts.append(nx)
                    pts.append(ny)
                color = "#ff00ff" #pink is for values out of range -1,..10
                if self.board[p][q] in self._colors:
                    color = self._colors[self.board[p][q]]
 
                draw.polygon(pts,fill=color)
                pts.append(pts[0])
                pts.append(pts[1])
                draw.line(pts,fill="black", width=1)
                draw.text([cx-3,cy-3], "{} {}".format(p,q), fill="black", anchor="m")
        img.save(filename)
 
 
    def a2c(self,p,q):
        x = p
        z = q
        y = -x -z
        return x,y,z
 
    def c2a(self, x,y,z):
        p = x
        q = z
        return p,q
 
    def distance(self,p1,q1,p2,q2):
        """ return distance between two cells (p1,q1) and (p2,q2) """
        x1,y1,z1 = self.a2c(p1,q1)
        x2,y2,z2 = self.a2c(p2,q2)
        dist = (  abs(x1-x2) + abs(y1-y2) + abs(z1-z2) ) // 2
        return dist
 
    def getScore(self, whichPlayer):
        """ return number of cells for given player """
        count = 0
        for p in self.board:
            for q in self.board[p]:
                if self.board[p][q] == whichPlayer:
                    count += 1
        return count
 
    def move(self):
        """ this method will be called by Brute. Return one of these:
            None -> if you CANNOT place any stone
            [ [p1,q1], ... [pn,qn] ] list of absolut (p,q) coordinates where you want to place a stone.
 
            For example, if you want to place one-cell stone to position p=1, q=-2:
            return [ [1,-2 ] ]
 
            If you want to place '3-cell-I-stone' at (1,1), (2,1) and (3,1):
            return [ [1,1] , [2,1], [3,1] ]
 
            If your start position is not filled, you have to start there!!
 
            startp, startq = self.getStartCoordinates(self.player)
            if self.board[startp][startq] == 0 -> you have to place first stone there!!
        """
        return None

Hráč

  • Hráč je implementován ve třídě Player, která je potomkem třídy Base
  • Třída Player musí být v souboru player.py
  • Jeden tah vašeho hráče je realizován metodou move():
    • move() vrací absolutní souřadnice kamene, který chce hráč položit na hrací desku ve formátu [ [p1,q1], … [pn,qn] ] nebo [] pokud nelze žádný kamen umístit
    • Příklad: umístění kamene se dvěma buňkami (0,0) a (1,0): Ve funkci move použijte return [ [0,0],[1,0] ]
  • Hráč si může (ale nemusí) zapsat umístěné kameny do své hrací desky.
  • Poté, co hráč vrátí kameny metodou move(), provede Brute aktualizaci hracích desek obou hráčů (ukázka v game.py)
  • Vykonání funkce move() a konstruktoru třídy Player je časově omezeno (viz timeout v sekci Pravidla hry)
  • V konstruktoru Player vyplňte proměnnou self.algorithmName - uložte si tam jméno svého algoritmu (používá se v turnajovém módu)
    • self.algorithmName = “muj skvely program cislo 1”

Soubor player.py

import base
 
class Player(base.Board):
    def __init__(self, player, size, stones):
        base.Board.__init__(self, player, size, stones)
        self.usedStone = [False]*len(self.stones) #all stones are free to use now 
 
        self.algorithmName = "Gemblo master!"
 
    def move(self):
        """ return list of absolut values to board where you want to place a stone 
            or [] if no stone can be placed 
        """
        startp, startq = self.getStartCoordinates(self.player)
        if self.board[startp][startq] == 0:
            #place some stone so the coordinates (startp, startq) is occupied
            #example:
            return [ [startp, startq] ]
        else:
            #place some stone anywhere according to rules
            #example:
            return []
 
 
if __name__ == "__main__":
    print("player")

Test hráče

  • Pro testování vašeho hráče použijte game.py (ve stejném adresáři, jako je váš player.py)
  • V tomto programu hraje váš hráč sám proti sobě
  • Herní smyčka v souboru game.py je záměrně zjednodušena a předpokládá, že váš hráč hraje správně dle pravidel a že během hry nespadne
  • Skutečná herní smyčka, která je realizována na Brutovi, kontroluje správnost tahů, timeouty, dodržování pravidel, atd.
  • Hru spustíte: python3 game.py

Soubor game.py

import base
import player  #student's player
 
"""
This program is a simplified version of the Gemblo game between two players.
The game is simplified by assumptions:
a) all players return valid stones that point to board 
b) no error occurs when calling .move
c) rules of the games are NOT checked here
 
The points a-c are however checked by Brute.
 
"""
 
 
 
size = 11
 
stones = base.loadStones("stones.txt")
 
p1 = player.Player(1, size, stones)  #player no. 1
p2 = player.Player(-1, size, stones) #player no -1
 
iteration = 0
while True:
 
    move1 = p1.move()  #move1 should be a list of stones or [] if no stone can be placed
 
    base.updatePlayers(p2, move1, p1.player)  #write stones to board of both players
    base.updatePlayers(p1, move1, p1.player)  
 
    p1.saveImage("{}-a.png".format(iteration))
 
 
    move2 = p2.move()
 
    base.updatePlayers(p1, move2, p2.player)  #write stones to board of both players
    base.updatePlayers(p2, move2, p2.player)  
 
    p2.saveImage("{}-b.png".format(iteration))
    iteration+=1
 
 
    if len(move1) == 0 and len(move2) == 0:
        print("End of game, both players return [] ")
        break
 

Hodnocení a turnaj

  • Hodnocení hráčů bude dvojí - ve hře student vs Brute, a posléze v turnaji každý s každým
  • Hra proti Brutovi:
    • Hráč na Brutovi je 'hloupý ale rychlý'. Jeho cílem není vás porazit, ale kontrolovat validnost tahů.
    • Bude se hrát jak z prázdné hrací desky (cca 10% případů) tak hlavně z již rozehraných desek (aby jsme co nejvíc otestovali správnost vašich programů)
    • Cílem není vyhrát, ale dohrát hru bez chyby.
    • Pokud vše proběhne bez chyby, dostanete 2 body.
    • V případě jakékoliv chyby je hodnocení 0 bodů.
  • Hra každý-s-každým
    • Budeme realizovat mimo Bruta, turnaj spustíme cca 1x denně
    • Každý student bude proti každému hrát 20x jako začínající (hráč 1) a 20x jako hráč -1
    • Po skončení bezchybné hry dostanou oba hráči tolik bodů, kolik je součet políček jejich barev
    • Pokud jeden z hráčů udělá chybu, hra se přerušuje. Hráč, který chybu způsobil je vyloučen z turnaje a jeho hry se nepočítají.
    • Výsledné bodování se provede pouze pro bezchybné hráče takto:
      • První 10% hráčů (dle nasbíraných bodů) dostane +3b
      • Dalších 20% hráčů (dle nasbíraných bodů) dostane +2b
      • Dalších 20% hráčů (dle nasbíraných bodů) dostane +1b
    • Ostatní hráči nedostanou žádné body navíc.
    • Pokud hráč obdrží proti Brutovi dva body, ale v turnaji selže, zůstavají mu dva body.
courses/b3b33alp/cviceni/t09.txt · Last modified: 2020/12/18 19:25 by vonasvoj