<?PHP
//This is slower than htmlentities() or htmlspecialchars(), but we actually want a subset of that functionality.
function sanitizeRequest($r) {
$request = array();
foreach ($r as $k => $v) {
$k = preg_replace("/[^A-Za-z]/", '', $k );
$request[ strtolower($k) ] = strtolower($v);
$v = preg_replace("/[^A-Za-z]/", '', $v );
}
return $request;
}
class Player {
protected static $alphabet = 'abcdefghijklmnopqrstuvwxyz';
protected $name = 'NAME';
protected $turns = 0;
protected $history = '';
// SEEMINGLY UNUSED
protected $opponents_word = '';
protected $letter_list = array();
protected $time_started = 0;
protected $letters_guessed = '';
protected $last_word = '';
// SEEMINGLY UNUSED
protected $hidden_word = '';
function __construct ($player_name = NULL, $sid = NULL) {
if ( ! isset($player_name)) {
$this->name = (isset($player_name)) ? $player_name : $this->generateString();
}
$this->initLetterList();
$this->chooseWord();
$this->time_started = time();
$ip = Utility::get_user_ip();
// error_log("New word for user @$ip: ". $this->opponents_word);
echo "New word -> " . $this->opponents_word ;
// ABOVE LINE MUST BE REMOVED when going live!
$_SESSION['player_object'] = $this;
}
//Take the alphabet and break it into an array of [letter] => [knowledge state]
function initLetterList() {
$aa = str_split(static::$alphabet);
$this->letter_list = array_fill_keys($aa, -1);
}
//Get a random word out of the list of 5-letter words
//INTERVENE HERE to deal with 1 or 2 NEW LISTS!!
function chooseWord() {
global $master_word ;
$split_word = array();
//Get words out of the word list file.
$wordlist = $this->getWordList();
//Choose a word from the list. (Changed SL's code to OK repeat letters! by remming out the while just below.)
// while ( count(array_unique($split_word)) != 5) {
$word = (array_rand($wordlist));
$master_word = $wordlist[$word];
// TEMPORARY, of course:
$master_word = "paean" ;
// Next seems not to be USED in any way
// $split_word = str_split($word);
// }
//Save the word we ended up with.
$this->opponents_word = $master_word;
return $master_word;
}
//Retrieve the word list from its file.
//Todo: Save to memcache.
function getWordList() {
$wordlist = file(__dir__ . '/words.txt', FILE_IGNORE_NEW_LINES);
if ($wordlist === FALSE) {
throw new Exception('words.txt missing');
}
return $wordlist;
}
function getGuessableList() {
$wordlist = file(__dir__ . '/guessable.txt', FILE_IGNORE_NEW_LINES);
if ($wordlist === FALSE) {
throw new Exception('guessable.txt missing');
}
return $wordlist;
}
//Standard battery of setters and getters
function getOpponentsWord() { return $this->opponents_word; }
function getTurns() { return $this->turns; }
function getName() { return $this->name; }
function getHistory() { return json_decode($this->history, TRUE); }
function getLastWord() { return $this->last_word; }
function getLetterList() { return $this->letter_list; }
function getTimeStarted() { return $this->time_started; }
function getLettersGuessed() { return $this->letters_guessed; }
function setLetterList( $new ) { $this->letter_list = $new; }
function setLastWord($last_word) { $this->last_word = $last_word; }
//Save the letters in all of the words they've guessed in a string; to be compared later.
// REALLY - Where/how is the used?
function setLettersGuessed( $word ) {
$letters_guessed = $this->letters_guessed;
$letters = str_split($word);
if (empty($letters)) {
return;
}
foreach ($letters as $num => $word_letter) {
if (strpos($letters_guessed, $word_letter) === FALSE) {
$this->letters_guessed .= $word_letter;
}
}
return;
}
function incrementTurns() {
$this->turns += 1;
return $this->turns;
}
// Take the word as it's guessed, do a last-minute check to compare the guess to the existing list (excluding a "repeater") and toss it on the stack.
function addHistory($word, $matching) {
$h = array();
if (!empty($this->history)) {
$h = json_decode($this->history, TRUE);
}
if (array_key_exists($word, $h)) {
$this->err_msg = 'That word has already been guessed.';
} else {
$this->err_msg = '';
}
$this->incrementTurns();
$this->last_word = $word;
$h[$word] = $matching;
$this->history = json_encode($h);
return $this->history;
}
// THIS LOOKS LIKE IT NEEDS major changes or elimination.
//Generate a string from random characters; this is not optimized for anything like pronounceability.
function generateString($max = 10) {
$name = '';
$length = strlen(static::$alphabet) - 1;
for( $i = 0; $i < $max; $i++ ) {
$name .= static::$alphabet[ mt_rand(0, $length) ];
}
// This next is temporary.
$name = "PETER" ;
return ucfirst($name);
}
}
class Game {
protected static $alphabet = 'abcdefghijklmnopqrstuvwxyz';
protected $last_turn = '';
//All exceptions get caught and handled here.
function wrapper( $request = NULL) {
try {
// if (isset($request['restart'])) {
// session_destroy();
// session_start();
// }
$response = $this->init( $request );
} catch (Exception $e) {
return ('EXCEPTION: ' . $e->getMessage());
}
return $response;
}
//Create or access an existing player, then perform all relevant business logic, and display the alphabet and history as needed.
function init( $request ) {
$p1 = $this->getPlayer( $request );
$response = $this->delegateInput( $p1, $request );
// echo $this->displayAlphabet($p1);
// echo $this->displayHistory($p1, TRUE);
return $response;
}
function getPlayer( $request ) {
$name = (isset($request['name'])) ? $request['name'] : '';
$p1 = isset($_SESSION['player_object']) ? $_SESSION['player_object'] : new Player( $name );
return $p1;
}
//Perform business logic as mandated by $_REQUEST vars.
function delegateInput( $player, $request ) {
if (isset($request['guess'])) {
// WEIRD STRAGGLER FOLLOWS!
// error_log('>> guess');
$response = $this->makeGuess( $player, $request );
$this->lastTurn($player);
} elseif (isset($request['letter'])) {
// HUH??
error_log('>> letter');
$this->changeAlphabet($player, $request['letter'][0] );
$response = '';
$this->lastTurn($player);
} elseif (isset($request['resetalphabet'])) {
// error_log('>> resetalphabet');
$player->initLetterList();
$this->reZeroNulls($player);
$response = 'Resetting alphabet state.';
$this->lastTurn($player);
} elseif (isset($request['resign'])) {
error_log('>> resign');
$response = $this->userLoses($player);
} else {
// error_log('>> default');
$response = 'Enter a 5-letter word to begin.';
//throw new Exception('Valid input missing: "guess" or "letter" required.');
}
// error_log('...' . $response);
return $response;
}
// NOT USING THIS FUNCTION at present.
function userLoses($player) {
session_destroy();
session_start();
$turns = $player->getTurns();
$word = $player->getOpponentsWord();
error_log("Resigned: User @".Utility::get_user_ip().": ". $word." after $turns turns.");
return "Giving up. Word was <a href='https://www.google.com/search?q=define+$word'>$word</a>. Enter another word to start again.";
}
//Get all of the words guessed by the player; color-code them by letter status (yes, no, unknown).
// LOOK AT NAME OF FUNCTION for its ... function!
// We eliminated what looked like only sometimes color-coding from this function.
function displayHistory($player) {
global $master_word ;
$letter_list = $player->getLetterList();
$history = $player->getHistory();
$oa = array();
// Evades an error/warning from the FIRST "count"
// if ( count($history) > 0) {
if ( count($history ?? []) > 0 ) {
$i = 0; // dollar-i IS going through ALL N WORDS?
// dollar-word IS the word just guessed!
foreach($history as $word => $correct) {
$oa[$i]['word'] = $word;
$oa[$i]['correct'] = $correct;
$lword = str_split($word);
// Pretty sure this next ALWAYS goes 1-to-5!
foreach ($lword as $lk => $lv) {
$oa[$i]['letter'][$lk] = $lv;
// NOT USING the getLetterStatus FUNCTION until further notice!
// $oa[$i]['status'][$lk] = $this->getLetterStatus( $letter_list[$lv] );
/*
echo "OA-><br>";
echo "MasterWord | lv+lk->" . $master_word . "|" .$lv ."|" .$lk . "<br>" ;
echo "<br>";
*/
if ( $lv == substr($master_word,$lk,1) ) { $oa[$i]['status'][$lk] = "Green"; } else
{ $oa[$i]['status'][$lk] = "NOT_Green"; }
}
$i++;
}
}
// Let's start by comparing the number of Greens with the old "JOTTO-RED number" - i.e., dollar-letters. IF NOT EQUAL:
// E.g., PIANO vs. target PAEAN yields 1 and 3, respectively ...
// Toss out the green letter(s) and use the logic elsewhere re word-a & word-b on what's left
// to assign RED to those letters/positions failing the intersect test. YELLOW TO THE OTHERS!
return $oa;
}
// User inputs valid word that they're guessing; return number of correct characters.
// If the word guessed is their opponent's actual word (or an anagram!), they win.
function makeGuess($player, $request) {
global $master_word ;
$guess = $request['guess'];
if (strlen($guess) != 5) {
return "Your guess must be exactly 5 letters long.";
}
$wordlist = array_flip($player->getGuessableList());
if ( ! isset($wordlist[$guess])) {
return 'Retry with a real word.';
}
// NOT CLEAR what the reference to OPPONENTS WORD is all about.
$guessed = array_flip(str_split(trim($guess)));
$master_word = trim($player->getOpponentsWord());
$word = array_flip(str_split($master_word));
// $letters = count(array_intersect_key($guessed, $word));
// echo "<br>raw count TBR'd->" . $letters . "<br>" ;
// HERE WE NEED TO ADD CODE to deal with 2- and 3-peats in either GUESSED or WORD or BOTH.
// So we REPLACE the last substantive line above, of course, given the new stuff below.
$word_a = str_split($master_word);
$word_b = str_split($guess);
for ($i=2; $i<6; $i++ ) {
$let = $word_a[$i-1] ;
for ($j=0; $j<$i-1; $j++ ) {
if ( $let == $word_a[$j] ) { $word_a[$i-1] = chr ( ord($word_a[$i-1]) - 32 ) ;
$let = $word_a[$i-1] ;}
}
}
for ($i=2; $i<6; $i++ ) {
$let = $word_b[$i-1] ;
for ($j=0; $j<$i-1; $j++ ) {
if ( $let == $word_b[$j] ) { $word_b[$i-1] = chr ( ord($word_b[$i-1]) - 32 ) ;
$let = $word_b[$i-1] ;}
}
}
$letters = count(array_intersect($word_b, $word_a));
// This adds the guess to history & deals with the "unlikely" possibility that it cannot be added.
try {
$history = $player->addHistory($guess, $letters);
} catch (Exception $e) {
return $e->getMessage;
}
// Let's just stick with the 5-letters-match test - i.e., anagrams WIN!
if ($letters == 5 ) {
return $this->userWins($player, $word);
}
$player->setLettersGuessed( $guess );
//If their word had 0 right, go ahead and toggle their alphabet off for them.
if ($letters == 0) {
foreach ($guessed as $letter => $i) {
$this->changeAlphabet($player, $letter, 0);
}
//If their word had all the letters right, toggle all of the present letters as correct.
} elseif ($letters == 5) {
foreach ($guessed as $letter => $i) {
$this->changeAlphabet($player, $letter, 1);
}
}
return;
}
function lastTurn($player) {
$turns = $player->getTurns();
if ($turns == 0) {
return $this->last_turn = 'Enter a 5-letter word to begin';
}
$last_word = $player->getLastWord();
return $this->last_turn = "Your last guess was: $last_word ($turns turns)";
}
//The user has won; display a message and clean up.
function userWins($player, $word) {
$turns = $player->getTurns();
session_destroy();
$duration = time() - $player->getTimeStarted();
$minutes = (int) ($duration / 60);
$seconds = (int) ($duration % 60);
return "You won! ($turns turns; ".$minutes."m".$seconds."s)";
// error_log("Won: User @".Utility::get_user_ip().": ". $word." in $turns turns.");
exit;
}
// NOT USING THIS FUNCTION OR LOGIC OR ANYTHING!
//Display their alphabet back to them, color-coded by letter status
function displayAlphabet($player) {
$letters_guessed = $player->getLettersGuessed($player);
$alphabet = $player->getLetterList();
foreach($alphabet as $letter => &$status) {
$letter_status = $this->getLetterStatus($status);
$was_guessed = (strpos($letters_guessed, $letter) === FALSE) ? '' : 'was_guessed';
$status = trim("letter_list $letter_status $was_guessed");
}
return $alphabet;
}
// NOT USING THIS FOR NOW
//Simple translation function.
function getLetterStatus($status) {
switch($status) {
case -1:
return 'unknown';
case 0:
return 'absent';
case 1:
return 'present';
}
throw new Exception ('Invalid status code passed to ' . __class__);
}
//Cycle through letter status - I think this means dealing with the 26 lettters & what's known about them.
function changeAlphabet($player, $letter, $change_to = NULL) {
$letter_list = $player->getLetterList();
if ($change_to === NULL) {
if ($letter_list[$letter] >= 1) {
$letter_list[$letter] = -1;
} else {
$letter_list[$letter]++;
}
} else {
$letter_list[$letter] = $change_to;
}
$player->setLetterList( $letter_list );
return $letter_list;
}
// NOT SURE WHAT THIS FUNCTION IS FOR.
function reZeroNulls($player) {
$h = $player->getHistory();
if (empty($h)) {
return;
}
foreach ($h as $word => $correct) {
if ($correct > 0) {
continue;
}
foreach (str_split($word) as $letter => $ignore) {
$this->changeAlphabet($player, $ignore, 0);
}
}
}
}
// This lets sftw handle multiple players!
class Utility {
function get_user_ip()
{
static $USER_IP;
$result = FALSE;
$fallback = false;
//fill the array with candidates IP from various resources
$ips = isset( $_SERVER['HTTP_X_FORWARDED_FOR']) ? explode(',',$_SERVER['HTTP_X_FORWARDED_FOR']) : array();
foreach( $ips as $i => $ip ){
$ip = trim( $ip );
$ips[ $i ] = $ip;
if( ! ip2long( $ip ) ) {
unset( $ips[ $i ]);
}
}
if( empty( $ips ) ){
if( isset( $_SERVER['REMOTE_ADDR'] ) ) {
$ips[]=$_SERVER['REMOTE_ADDR'];
}
if( isset( $_SERVER['HTTP_CLIENT_IP'] ) ) {
$ips[]=$_SERVER["HTTP_CLIENT_IP"];
}
}
foreach ($ips as $ip) { //for all the ips, work on it one-by-one based on patterns given down here
if (!preg_match("/^([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)/",$ip)) {
//if it doesn't match the pattern, skip
continue;
}
if( ! ip2long($ip) ) {
continue;
}
$result = $ip;
}
if ($result===false) {
$result = $fallback; //if fallback is not found, it will be false
}
if( $result === false ) {
$result = '0.0.0.0';
}
return $USER_IP = $result; //if all resources are exhausted and not found, return false.
}
}