/* * This file is part of gwap, an open platform for games with a purpose * * Copyright (C) 2013 * Project play4science * Lehr- und Forschungseinheit für Programmier- und Modellierungssprachen * Ludwig-Maximilians-Universität München * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package gwap.game; import gwap.game.memory.ForceBean; import gwap.model.GameType; import gwap.model.Person; import gwap.tools.Pair; import java.io.Serializable; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.ListIterator; import java.util.Random; import javax.persistence.EntityManager; import javax.persistence.Query; import org.jboss.seam.Component; import org.jboss.seam.annotations.Create; import org.jboss.seam.annotations.Destroy; import org.jboss.seam.annotations.In; import org.jboss.seam.annotations.Logger; import org.jboss.seam.contexts.Contexts; import org.jboss.seam.core.Conversation; import org.jboss.seam.faces.Switcher; import org.jboss.seam.international.LocaleSelector; import org.jboss.seam.log.Log; /* * Generic PlayerMatcher * * This class is used to pair two players (or a player and an Ai) * If no two human players are found after Player.WAIT_TIMEOUT and if Player.allowAi is true, * the player is paired with an Ai * * To create a custom game, subclasses of PlayerMatcher, SharedGame and Player need to be created * (See gwap.game.test for a minimalistic example) * * Furthermore, some actions have to be added to pages.xml * * An action for the lobby page to ensure players are registered with the playerMatcher * <page view-id="/memoryLobby.xhtml"> * <action execute="#{gwapGameMemoryPlayerMatcher.enqueue}" on-postback="false" /> * </page> * * A redirection rule for the game page and if applicable for the scoring page: * <rule if-outcome="memoryGame"> * <redirect view-id="/memoryGame.xhtml"/> * </rule> * */ public abstract class PlayerMatcher<G extends SharedGame<P>, P extends Player<P>> implements Serializable{ private static final long serialVersionUID = 1L; @Destroy public void destroy() { log.info("Destroying"); } @Logger private Log log; @In private EntityManager entityManager; @In private Switcher switcher; @In private LocaleSelector localeSelector; //private GameType gameType; @In(create=true) private Person person; // @In(create=true) private GameSessionBeanNew gameSessionBeanNew; //List of games that are currently queued (ie have only one player) private List<G> queue=new ArrayList<G>(); //List of games that are currently in progress (ie have two players, at least one of which is human) private List<G> activeGames=new ArrayList<G>(); /* Prefix for instantiation of classes used in a specific gametype: * gameName + "Player" * gameName + "SharedGame" * gameName + "Ai" * * Furthermore, GameType (roundDuration, etc) is read from database by using * gameType.name == gameName * * Finally, gamename is used by Highscore.xhtml to extract the * full name of the game from messages.xml * * * Example: gameName = "gwapGameMemory" * => Player subclass MUST have @name "gwapGameMemoryPlayer" * => SharedGame subclass MUST have @name "gwapGameMemorySharedGame" * => GameType in database MUST have name "gwapGameMemory" */ private String gameName; @Create public void init() { log.info("Creating"); } /*public GameType getGameType() { return gameType; }*/ public PlayerMatcher(String gameName) { this.gameName=gameName; } public Pair<G, P> match() { return match((G)Component.forName(gameName+"SharedGame").newInstance(), (P)Component.forName(gameName+"Player").newInstance()); } public Pair<G, P> match(G game, P player) { return match(game, player, gameName); } public Pair<G, P> match(G game, P player, String gameTypeName) { log.info("Enqueueing"); // get GameType Query query = entityManager.createNamedQuery("gameType.select"); query.setParameter("name", gameTypeName); GameType gameType = (GameType) query.getSingleResult(); game.setGameType(gameType); game.setLanguage(localeSelector.getLanguage()); game.setRoundCount(gameType.getRounds()); if (game.getMoveMode()) { if (gameType.getRoundDuration()!=0) game.setMoves(gameType.getRoundDuration()); } else game.setRoundLength(gameType.getRoundDuration()); Pair<G, P> pair=null; synchronized(queue) { synchronized(activeGames) { //Remove timed-out players and empty games cleanList(queue); cleanList(activeGames); Pair<G, P> queuedGame=isQueued(game); if (queuedGame==null) { log.info("Creating player for person "+person.hashCode()); Conversation.instance().begin(); player.setPerson(person); player.setSharedGame(game); player.setPlayerMatcher(this); // gameSessionBeanNew.startSession(gameName); player.setConversation(switcher.getConversationIdOrOutcome()); game.addPlayer(player); pair=pair(game); //return pair; } else { log.info("Person "+person.hashCode()+" is already playing"); //Restore the previous conversation id String conv=switcher.getConversationIdOrOutcome(); if (conv==null || !conv.equals(queuedGame.b.getConversation())) { switcher.setConversationIdOrOutcome(queuedGame.b.getConversation()); switcher.select(); } if (queuedGame.b.isMatched()) queuedGame.b.signal("justMatched"); return queuedGame; } } } //Players have been matched if (pair.b.isMatched()) { //Start potentially time consuming process (newRound) outside locked region pair.a.newRound(); //Outject new components to avoid race conditions in view Contexts.getConversationContext().set(gameName+"SharedGame", pair.a); Contexts.getConversationContext().set(gameName+"Player", pair.b); signal(pair.b); } return pair; } private G findPlayer(Person person) { ListIterator<G> g=queue.listIterator(); while (g.hasNext()) { G game=g.next(); P lplayer=game.getPlayerByIndex(0); if (lplayer.getPerson()==person) { return game; } } return null; } private Pair<G,P> updatePlayer(List<G> list, G cGame) { ListIterator<G> g=list.listIterator(); while (g.hasNext()) { G game=g.next(); P player=game.updatePlayer(person); if (player!=null) { if (game.isCompatible(cGame)) return new Pair<G, P>(game, player); else //Player is already playing or enqueued, but has request a new game { //Remove player player.forceTimeout(); //Remove the old if it is not currently matched if (list==queue) g.remove(); } } } return null; } //Finds out whether the current Person is already playing a game or is queued. private Pair<G, P> isQueued(G game) { Pair<G,P> queued=updatePlayer(queue, game); if (queued!=null) return queued; else return updatePlayer(activeGames, game); } private void cleanList(List<G> list) { Iterator<G> g=list.iterator(); while (g.hasNext()) { G game=g.next(); if (game.cleanPlayers() || game.isTerminated()) g.remove(); } } public void link(P match1, P match2, G game) { Random r=new Random(); boolean ids=false; if (match2.isAi()) ids=r.nextDouble()>0.8; //Make human player the describer with high probability else //Randomly assign human players ids=r.nextBoolean(); ForceBean forceBean=(ForceBean)Component.getInstance("gwapGameMemoryForceBean"); //Always start as Describer in forced Id matches if (forceBean!=null && forceBean.getForcedId()!=0) ids=false; //ids=true; if (ids) //Player 1 is Guesser { match1.setId(0); match2.setId(1); } else //Player 1 is Describer { match1.setId(1); match2.setId(0); } match1.setPartner(match2); match2.setPartner(match1); match1.setSharedGame(game); match2.setSharedGame(game); //match1.signal("justMatched"); //match2.signal("justMatched"); } public void signal(P player) { player.signalAll("justMatched"); } public Pair<G, P> pair(G game) { G match=null; Iterator<G> g=queue.iterator(); while (g.hasNext() && match==null) { G cgame=g.next(); if (cgame.isCompatible(game)) { match=cgame; g.remove(); } } if (match!=null) { P match1=match.getPlayerByIndex(0); P match2=game.getPlayerByIndex(0); match.addPlayer(match2); activeGames.add(match); link(match1, match2, match); //match.newRound(); return new Pair<G, P>(match, match2); } else { queue.add(game); return new Pair<G,P>(game, game.getPlayerByIndex(0)); } } public void MatchTimeOut(Person p) { log.info("Matching person #0 with AI", p); G game; P player; P ai; synchronized(queue) { synchronized(activeGames) { game=findPlayer(p); if (game==null) return; queue.remove(game); player=game.getPlayerByIndex(0); ai=(P)Component.forName(gameName+"Ai").newInstance(); game.addPlayer(ai); activeGames.add(game); link(player, ai, game); } } game.newRound(); player.signalAll("justMatched"); ai.poll(0); } }