|
25 Years of Programming
An open source source for C, C++, OWL, BASIC, MDB, XLS, DOT, and more... |
Home Projects Up Sitemap Search Blog Forum+Chat About Us Privacy Terms of Use Feedback FAQ Images Services Ads Donate Humor |
/* talk.cpp 5-23-00
This is distributed as part of the WTalk project, but it is an earlier version
(legacy code). The target is Borland's EasyWin: it is a Windows 3.1 target, but the
program runs in a console interface that operates as if it were a DOS console.
Copyright (C)1993-2000 Steven Whitney.
Published under GNU GPL (General Public License) Version 2, with ABSOLUTELY NO WARRANTY.
Initially published by http://25yearsofprogramming.com.
an attempt at a program that acquires and uses knowledge about the world
to carry on an "interesting" conversation with user.
EasyWin target.
This version is being increasingly left behind by WTALK, and it may at any
time get left with methods that are no longer the same, and could
become incompatible with its data files.
Particular changes not implemented here:
Log, log() and its writing to State and its removal in STalk dtor
ExecAction handling of showstate
WTalk no longer allows CmFileOpen to be called from ExecAction,
which allows it to respond to the file's contents.
5/19/2006
I don't recall in what state I left this program. It was the last version before I converted
the entire program to Borland OWL, and on inspection, the similarities between this and
WTalk.cpp are obvious. I believe that for a time this was side-by-side compatible
with WTalk in the sense that it appears to use the DIC, FACT, TOK, RULE and other utility
classes in the same ways, and in fact shared the same source code for those classes. It's
possible that this still has the same 100% compatibility, but subsequent work on the utility
classes for their use with WTalk was probably performed without any consideration for whether
it might break this program.
------
to do:
all to do are in wtalk.cpp
------
notes:
--most notes related to this program are now in talk.doc.
--see note in todo.doc about scanning dictionary entries for possible use.
*/
#include <stdio.h>
#include <conio.h>
#include <classlib\time.h>
#include "c:\bcs\my.h"
#pragma hdrstop
#include "dic.h"
#include "fact.h"
#include "clasfier.h"
#include "c:\bcs\library\filearay.cpp"
//////////////////////////////////////////////////////////////////////////////
// Talker converses with user.
class Talker
{
public:
Talker(); // constructor
~Talker(); // destructor
// functions
void help();
void Converse();
// variables
protected:
// functions
void PreprocessUserInput(string& s);
void sense();
string respond();
string CmRuleCreate();
string Execute(const string& name);
int SetVariable(const string& name, const string& value);
// response subunit functions; each needs a line in Execute()
// note there are also some ordinary fns that may be used as subunits
string PostUserName();
string askaboutpreviousstatement();
string restateusersentence();
void CmFileOpen();
// variables
FactMemory Facts;
RuleSet Rules;
// state indicator variables, measuring state of environment, state of self.
// avoid permanent variables if possible: keep them as state variables in RuleSet.State
// any member variables here must be read on startup and written in destructor
};
//----------------------------------------------------------------------------
// constructor
Talker::Talker()
{
backupfile("dialog.usr"); // each talker will need its own file
Rules.State.Add(StateNode("usrname","USER")); // default name for user
} // constructor
//----------------------------------------------------------------------------
// destructor
Talker::~Talker()
{
} // destructor
//----------------------------------------------------------------------------
// command interpreter. Receives the English name of a function, variable, or literal text.
// translates it into an actual call of the function with that name.
// To allow a new function to be handled, just add a line to handle it.
// returns a string because a string can return a string OR any numeric value.
// See notes below in "subunit functions" section.
// Eventually, Execute should also record into some sort of log the names of the steps it
// executes, for building and mutating a new procedure by trial and error.
string Talker::Execute(const string& name)
{
if(name == "PostUserName") { PostUserName(); return ""; }
if(name == "askaboutpreviousstatement") { return askaboutpreviousstatement(); }
if(name == "restateusersentence") { return restateusersentence(); }
if(name == "help") { help(); return ""; }
if(name == "showdic") { return TOK::Dic.Show(); }
if(name == "showmachines") { return FACT::ShowMachines(); }
if(name == "CmFileOpen") { CmFileOpen(); return "End of File"; }
// #error this ("toks") is now the last!: show next to last
if(name == "toks") { return Facts.ShowLast(); }
if(name == "showstate")
{
ostrstream os;
os << Rules.State << endl << ends;
string s = os.str();
delete[] os.str();
return s;
}
if(name == "answerq") { return Facts.answerq(); }
// output flagged facts: an example of a normal internal function used as a subunit,
// "converted" to return string, and it has a synonym!
if(name == "ShowFacts") { return Facts.ShowFacts(TRUE); }
if(name == "outputflaggedfacts"){ return Facts.ShowFacts(TRUE); }
if(name == "showallfacts") { return Facts.ShowFacts(FALSE); }
// return values of variables. anything could be a state variable to be looked up:
// if a value is found for name, return it, else fall through to string literal output.
string value = Rules.State.GetValue(name);
if(value.length())
return value;
return name; // name unknown: interpret as literal output,
} //Execute
//----------------------------------------------------------------------------
// Receives the English name of a variable, a new value for it (as text), and
// translates it so that the actual variable with that name is set to the new value.
// to allow a new variable to be handled, just add a line to handle it.
int Talker::SetVariable(const string& /* name */ , const string& /* value */ )
{
// if(name == "var1") { fromstring(var1,value); return(TRUE); }
return(FALSE);
} //SetVariable
//---------------------------------------------------------------------------
// user creates a new Rule. modified from GKEY::AddKey()
// Currently it must be a keyword-responder rule, but could expand to allow (and assist)
// creation involving other tested conditions (IfNodes) and Actions.
// (low priority because you can also just add a rule to RULES.DAT)
// Although providing an explicit rule seems like cheating, it is analogous to
// *showing* someone how to do something, which you can't do for the pgm.
// Here, you are simply using *its* language to show it how.
//
// previous is latest user sentence, which is (may be) in need of a better reply.
// in later versions, the better reply may be based on things other than s.
// returns the better reply so it can be used right away.
string Talker::CmRuleCreate()
{
string previous = Rules.State.GetValue("s");
int importance = 1000;// default value for new Rule's bid value, in case of entry error
string newword; // new keyword or phrase to add
string newreply; // new reply to add
cout << "I must have missed something, or even said something stupid!\n";
cout << "You can now tell me what I should have said.\n\n";
do
{
cout << "You said:\n" << previous << "\n\n";
cout << "Please pick a word or phrase from your sentence that I should watch for.\n";
cout << "It should be a significant one. When I see this word or phrase,\n";
cout << "I'll consider using the response. What is the word or phrase?\n: ";
getline(cin,newword,'\n');
newword.to_lower();
cout << "\n\nPlease type the reply I should have given.\n";
cout << "Type it exactly the way I should have, including upper/lower case.\n: ";
getline(cin,newreply,'\n');
cout << "\n\nWhen I saw the word(s):\n" << newword << "\nI should have answered:\n";
cout << newreply << "\nIs this correct (y/n)? ";
}
while(tolower(getche()) != 'y');
cout << "\n\nHow important is this word when I encounter it?\n";
userinput("1=least 32000=most, NORMAL=1000: ",importance);
cout << "\nThank you. I'm making the change.\n";
// just in case, strip any quotes from user entries and replace them with single quotes
size_t u;
while((u = newword.find("\"")) != NPOS) newword.put_at(u,'\'');
while((u = newreply.find("\"")) != NPOS) newreply.put_at(u,'\'');
// create and add the new rule
Rule r(importance);
r.IfNodes.Add(StateNode("s",newword,CONTAINS));
r.Actions.Add(newreply);
// IF this fn CAN ONLY be called during "repeat" handling, then the real winning rule
// has already received its negative feedback and had wasawinner cleared, so we can
// pretend that this new Rule was the winner. After pgm says, "so I'll try again, and say."
// the next usrscore will be credited to it.
r.wasawinner = TRUE;
Rules.Add(r);
return(newreply);
} //CmRuleCreate
//--------------------------------------------------------------------------
// post any state messages that haven't already been posted elsewhere.
// MOST SHOULD INSTEAD BE POSTED AS SOON AS YOU HAVE A VALUE FOR THEM.
// whenever a value to be posted might not HAVE a valid value to post,
// make provisions to clear any leftover obsolete value it now has.
// use StateArray::Remove(name), or post "" or "00000",
// whichever is less likely to APPEAR meaningful and get misunderstood.
void Talker::sense()
{
// The added facts may contain a combination of questions and non-questions.
// statevar sentype only records the type of the latest FACT. If any embedded questions
// remain unhandled, they will accumulate in Facts, but eventually get answered.
string s = Rules.State.GetValue("s"); // the raw s was posted in its entirety.
Rules.State.Add(StateNode("slenchars",s.length()));
int newfacts = Facts.AddFacts(s);
if(newfacts) // if valid fact(s) were successfully added
cout << Facts.ShowLast(); // auto-display most recent user sentence in fact format
// tally TOTAL lengths of user's response, which may have had multiple facts.
// slenwords = word count = sum of tokcount from all the just-added facts.
// NOTE: s and slenchars (added above) are raw (from s), but slenwords
// is from FACT, which has added a terminator if it didn't have one.
// Avoid rules that test s==something, because user may or may not have typed terminator.
uint factcount = Facts.GetItemsInContainer();
Rules.State.Add(StateNode("factcount",factcount));
uint slenwords = 0;
for(int i = factcount - newfacts ; i < factcount ; i++)
slenwords += Facts[i]->GetItemsInContainer();
Rules.State.Add(StateNode("slenwords",slenwords));
if(newfacts)
{
FACT* lastfact = Facts[factcount - 1];
Rules.State.Add(StateNode("sentype",lastfact->sentype));
if(lastfact->sentype == FACT::QUESTION)
Rules.State.Add(StateNode("qtype",lastfact->qtype));
else
Rules.State.Remove("qtype");
}
else
{
}
} //sense
//---------------------------------------------------------------------------
/*
THIS SECTION HAS THE SUBUNIT FNS FOR CREATING DOCTOR'S REPLIES.
1. AVOID fn names that are the same as literal text you might need to output!
2. Each fn, if accidentally called illegally, when it can't do what it's supposed to,
must only do something benign, and not crash!
3. Each fn must have a line for it in Execute(). If it returns anything besides string,
you can use tostring() in Execute to cast the result, or return an arbitrary string.
4. Each fn MUST take no parms. If it needs parms, it must obtain the info
from members, etc.
5. Each fn should have in RULES.DAT at least one protected rule that invokes
it in its action list, so it can be dragged into action lists of new rules.
*/
//--------------------------------------------------------------------------
// get user's name and post it as a state variable
// Study this fn. If you can just (effectively) get a parameter passed to it
// (through a member?), it can be a template for a more general method that asks
// user for a piece of info, gets it, then posts it as a StateVariable,
// where it becomes available for general use.
// It's a way for getting and making USABLE the users' answers to questions the program ASKS.
string Talker::PostUserName()
{
string usrname = Rules.State.GetValue("s");
// #error you need to strip off such phrases as "my name is"
titlecase(usrname);
Rules.State.Add(StateNode("usrname",usrname));
return(usrname);
// old
// string usrname;
// do
// {
// cout << "\n\nWhat is your name? ";
// getline(cin,usrname,'\n');
// }
// while(!usrname.length());
// titlecase(usrname);
// Rules.State.Add(StateNode("usrname",usrname));
// return(usrname);
} //PostUserName
//---------------------------------------------------------------------------
// ask about something user previously said, to restart flagging conversation.
// could select only a portion (subj,verb,obj), OR find topics mentioned > once.
string Talker::askaboutpreviousstatement()
{
static int factpos = 0;
string response = "Please say something. I'm out of questions to ask!"; // last resort
string t;
while(1)
{
if(factpos >= Facts.GetItemsInContainer()) // none, or none left (all used)
return(response);
t = Facts[factpos++]->Sentence;
if(t.length() >= 10) // if too short, loop again to get another one
break;
}
response = string("Earlier, you said: ") + t + " Please tell me more.";
return(response);
} //askaboutpreviousstatement
//--------------------------------------------------------------------------
// restate user's latest response, but reverse subject context
string Talker::restateusersentence()
{
string response = "Please continue."; // last resort
uint i = Facts.GetItemsInContainer();
if(i)
response.prepend("You said " + Facts[i - 1]->ReversedContext() + " ");
return(response);
} //restateusersentence
//---------------------------------------------------------------------------
// END OF RESPONSE SUBUNIT FUNCTION SECTION
//----------------------------------------------------------------------------
// RESPOND() create the doctor's responses
string Talker::respond()
{
// post any state variables that haven't already been set when their values became known.
sense();
//-----
// UNSURE IF THESE HAVE ANY USE HERE
// refill to starting capacity with random rules (guesses)
// Rules.TopUp(FALSE,FALSE);
// generate new rule children. preferentially selects bidders as parents.
// Adding the rule may cause deletion of the weakest unlocked rule that isn't a source.
// if(!random(REPRODUCERATE))
// Rules.Addchild(TRUE);
//----------
// find and record the previous winner (if any) so it can be the next winner's source
Rule* prevwinner = Rules.Winner(); // 0 if none
// now it is ok to reset wasawinner and source for all rules.
// don't reset isabidder because we may have been accumulating bidders.
Rules.ZeroFlags(TRUE,FALSE,TRUE);
//----------
// IDENTIFY WHICH RULES WILL BE BIDDERS IN THE UPCOMING LOTTERY.
while(!Rules.MarkBidders() && Rules.State.GetItemsInContainer())
{
cout << "MarkBidders failed." << endl;
// if there were no bidders, add some new Rules,
// one tailored to respond to each of the current StateArray states?
// but it doesn't matter: with the protected rules, there will always be a bidder,
// and this will never be executed. So where should new rules be made?
// int N = Rules.State.GetItemsInContainer();
// for(int i = 0 ; i < N ; i++)
// Rules.AddCustom();
}
//----------
cout << "Bidders: ";
for(uint m = 0 ; m < Rules.GetItemsInContainer() ; m++)
if(Rules[m]->isabidder)
cout << Rules[m]->id << " ";
cout << endl;
//----------
// LOTTERY: DETERMINE WINNING RULE
Rule* winner = Rules.Lottery(TRUE); // returns zero if no winner, no bids
if(winner) // there must, and will, be a winner.
{
cout << "Winner: " << winner->id << endl;
// winner's source is the previous winner, on the idea that maybe the previous
// response provided the setup to make the current one possible,
// and if the current one got a high score, it should pass back some of the credit.
// The goal is chains of dialog that eventually produce a high user score.
winner->source = (prevwinner ? prevwinner : winner); // safe! pays itself.
// pay source when it wins the right to post (now)
winner->paysource(100); // for now, very small passback (1/100)
// lottery() should NOT set these. lottery is also used for other purposes.
winner->wincount++; // increment # of times it has won
winner->wasawinner = TRUE; // remember we won
winner->isabidder = FALSE; // can't win again
}
// this is the one location where we should reset isabidder: the lottery is done.
// be sure to preserve wasawinner, used much later when we get user feedback on our output
Rules.ZeroFlags(FALSE,TRUE,TRUE);
//----------
// CARRY OUT THE WINNING RULE'S ACTIONS
string response; // function's return value
// for now, multiple actions just extend the output string.
for(int i = 0 ; i < winner->Actions.GetItemsInContainer() ; i++)
response += Execute(winner->Actions[i]) + " ";
response = response.strip(string::Both);
// get old pgm response and repost it as pgmprev (previous)
Rules.State.Add(StateNode("pgmprev",Rules.State.GetValue("pgmlast")));
Rules.State.Add(StateNode("pgmlast",response)); // post response as soon as we have it
ofstream("dialog.usr",ios::app) << "PGM: " << response << endl; // AND write to dialog file
// experimental, and possibly risky:
// anything we were going to do with pending input s is done, so
// delete it. Now GetValue(s).length can be a flag for WHETHER there is any pending user
// input awaiting a response, which can become just another factor used in consideration
// of what to do next. If you later have idle processing, this guarantees you don't
// respond 1000 times a second to the same old s!, and that you don't waste time trying
// to respond to s if there isn't one.
// Drawbacks: this may not be best place to do it, and now s can't be found at all
// (including during new rule creation).
// don't enable this until there is some real reason to do so.
// Rules.State.Remove("s");
return(response);
} //respond
//---------------------------------------------------------------------------
void Talker::help()
{
cout << endl;
cout << "DIC Display entire dictionary." << endl;
cout << "BYE End the session." << endl;
cout << "HELP Display this help screen." << endl;
cout << "LIST or FACTS List facts database." << endl;
cout << "READFILE Read lines from a file." << endl;
cout << "TOKS Show tokens from latest sentence." << endl;
cout << "MACHINE Show state machine contents." << endl;
cout << "STATES Show state variables (StateArray)." << endl;
cout << endl;
cout << "Sample valid queries:" << endl;
cout << "WHO (verb phrase)?" << endl;
cout << "WHAT DID (noun phrase) DO?" << endl;
cout << "WHY DID (noun phrase) (verb phrase)?" << endl;
cout << endl;
presskey();
cout << "Scores (0-5): These allow you to rate how the program's response affected" << endl;
cout << "your interest in the conversation, for any reason. The program can't" << endl;
cout << "see or hear your reactions, so this is its substitute." << endl;
cout << endl;
cout << "If its reply was coherent, or on the topic, or appropriate, or you found" << endl;
cout << "it funny for any reason, you should give it a good score. It uses your" << endl;
cout << "scores to decide how, when, or whether to ever use that response again." << endl;
cout << "Guidelines:" << endl;
cout << "0 Worst. Use only to indicate unintelligible nonsense" << endl;
cout << endl;
cout << "1 Bad" << endl;
cout << "2 Poor" << endl;
cout << "3 Average or Neutral" << endl;
cout << "4 Good" << endl;
cout << "5 Excellent" << endl;
cout << endl;
presskey();
} // help
//--------------------------------------------------------------------------
// preprocess an incoming string to put it into a standardized format.
void Talker::PreprocessUserInput(string& s)
{
if(!s.length())
return;
for(int i = 0 ; i < s.length() ; i++)
{
if(iscntrl(s[i])) // change any control chars to spaces
s[i] = ' ';
else // [] are now the delimiters in rules,
if(s[i] == '[') // so they are prohibited in incoming text,
s[i] = '('; // to avoid any chance of becoming embedded in saved rules
else // or in states.dat.
if(s[i] == ']')
s[i] = ')'; // Can change to parentheses or curly braces.
}
// It seems odd to have contraction expansion and unknown word lookup here, but if you wait
// until you've exhausted all lookup avenues and *have* to ask the user, it's
// difficult because the user input is platform-dependent.
// Also, you know that you don't know a word the instant you hear it.
uint sp = 0; // index into s: token search starting point
string token; // token from sentence
if(s.contains("'")) // contraction expansion: quick initial test
{
string s2; // for building expanded s into
while((sp = gettoken(s,sp,token)) != 0) // gettoken returns new place to start from
{
if(!token.contains("'")) // can't be a contraction
{
s2 += token + " "; // just add the word
continue;
}
int i = FACT::Contractions.Find(token); // it now ONLY searches, no add.
if(i != INT_MAX) // it was a known contraction
{
s2 += FACT::Contractions[i].Sub + " "; // append expansion string
continue;
} // if it falls through, it MIGHT be a contraction
// determine if token is a contraction, and get its expansion from user
if(yesno(to_upper(token) + " contains an apostrophe. Is it a contraction"))
{
string t;
string e1, e2;
while(!t.length()) // only an empty expansion is illegal
{
cout << "(Please use lower case, unless a word SHOULD always be capitalized.)\n";
cout << "Please enter the two words that it expands to, separated by a space:\n";
cout << to_upper(token) << " means: ";
char userbuf[80];
gets(userbuf);
istrstream(userbuf) >> e1 >> e2;
t = e1 + " " + e2;
}
s2 += t + " "; // expansion text
if(t != token) // if different, add new contraction to ContractionList
FACT::Contractions.Add(TextSub(token,t)); // new
} // end if(it IS a contraction)
else
s2 += token + " "; // add original token
} // end while(there are tokens to process)
s = s2; // s2 contains all expansions made, if any
} // if(s.contains("'"))
//--------------------
// may be unnecessary, but just in case any more got in.
for(i = 0 ; i < s.length() ; i++)
{
if(iscntrl(s[i])) // change any control chars to spaces
s[i] = ' ';
else // [] are now the delimiters in rules,
if(s[i] == '[') // so they are prohibited in incoming text,
s[i] = '('; // to avoid any chance of becoming embedded in saved rules
else // or in Log or states.dat.
if(s[i] == ']')
s[i] = ')'; // Can change to parentheses or curly braces.
}
//--------------------
s = s.strip(string::Both); // strip all leading and trailing spaces
// s = is required, else s doesn't get changed.
size_t u;
while((u = s.find(" ")) != NPOS) // change any internal multiple spaces to single,
s.remove(u,1); // for string searches in FACT::parse()
// ask user about unknown words and add them to Dic
sp = 0; // begin new search
while((sp = gettoken(s,sp,token)) != 0)
{
// consider adding this somewhere:
// if a new word is capitalized within sentence, assume it's proper.
// No action needed, because it's already capitalized in DicEntry.
// if first word in sentence (capitalized) is newly added,
// AND user said it's a noun, ask user.
// you can't capitalize Great Lakes because it would capitalize Great and Lakes on disk.
// commented out because it's DOS-only *and* it's never happened.
// if(GetItemsInContainer())
// if(toks[0].lookup->ismodified)
// if(toks[0].lookup->HasType(NOUN))
// if(isupper(toks[0].lookup->alpha[0]))
// {
// // all lower, in case user had caps lock on (all capitalized!)
// toks[0].lookup->alpha.to_lower();
// if(yesno("Is the first letter of " + to_upper(toks[0]) + " always capitalized"))
// titlecase(toks[0].lookup->alpha);
// }
// if not found in memory or on disk, Lookup WILL create an entry, but it may be invalid
DicEntry* d = TOK::Dic.Lookup(token);
while(!d->TypeCount()) // the validity test is that it must have minimum 1 type
{
cout << "To add this word to dictionary: " << to_upper(token) << endl;
cout << "I need the part(s) of speech it can have:" << endl;
cout << "NOUN " << NOUN << " (nouns and pronouns)" << endl;
cout << "VERB " << VERB << endl;
cout << "VERB HELPER " << VBHELP << " (ex: to, will, would)" << endl;
cout << "ADJECTIVE " << ADJ << " (and possessives that can follow 'the')" << endl;
cout << "ADVERB " << ADV << endl;
cout << "DETERMINER " << DET << " (articles, and possessives that can't follow 'the')" << endl;
cout << "PREPOSITION " << PREP << endl;
cout << "TERMINATOR " << TERM << endl;
cout << "SUBORD. CONJ " << SCONJ << endl;
cout << "COORD. CONJ " << CCONJ << endl;
cout << "EXCLAMATION " << EXCL << " (exclamations or interjections)" << endl;
cout << "\nEnter numbers separated by spaces: ";
char userbuf[80]; // user input
gets(userbuf);
if(strlen(userbuf)) // user entered something
{
istrstream is(userbuf); // a stream to read from
int i;
while(is >> i) // while we can read an integer,
d->AddType(i); // add to possible-type list in dic
}
if(!d->TypeCount())
cout << "You must enter at least 1 valid type for this word." << endl;
d->ismodified = TRUE; // IMPORTANT! only this ensures write to disk at end.
}
}
// consider re-posting S here in expanded form.
// Rules.State.Add(StateNode("s",s));
} //PreprocessUserInput
//--------------------------------------------------------------------------
// read sentences from file as though they came from user.
// no rule scoring is done.
void Talker::CmFileOpen()
{
BOOL pause;
string filename;
ifstream infile;
do // get file name
{
cout << "\nFile name to read, or <CR> to abort: ";
getline(cin,filename,'\n');
if(!filename.length())
return;
infile.open(filename.c_str());
if(infile)
pause = yesno("Pause to see each sentence as it is read from the file");
else
cout << "Input file " << to_upper(filename) << " not found." << endl;
}
while(!infile);
string s;
ofstream outfile("dialog.usr",ios::app);
while(getsentence(infile,s)) // getsentence may add a terminator
{
PreprocessUserInput(s); // preprocess control chars,tabs,mult.spaces
cout << s << endl; // display line from file
Rules.State.Add(StateNode("s",s)); // post it, and write it to log file
outfile << to_upper(Rules.State.GetValue("usrname")) << "(" << filename << ")" << ": "
<< s << endl;
// if you DON'T call respond(), you MUST call sense() so facts go into Facts.
sense();
// CmFileOpen was called FROM respond: this will cause recursive entry, which I hadn't planned.
// check for possible problems it would cause.
// cout << "PGM: " << respond() << endl; // get and print response from "doctor"
if(pause)
presskey();
}
} //CmFileOpen
//----------------------------------------------------------------------------
// carry on a conversation
void Talker::Converse()
{
// cout << "PGM: " << respond() << endl; // say something (mainly so user can score it)
while(1) // dialog begins
{
string s; // user's sentence
//----------
// time the total of 2 delays: before giving score and before beginning reply.
clock_t usrdelay = clock(); // counts ticks (milliseconds): 65 secs max
//----------
// get score from user, post, and apply it to winning rule(s).
int usrscore = '0'; // user's rating of pgm's output
do
{
cout << "\nScore (0=Worst, 1=Bad, 2=Poor, 3=Avg, 4=Good, 5=Best): ";
usrscore = getch();
cout << (char)usrscore << endl << endl;
}
while(!string("012345").contains((char)usrscore));
usrscore -= '0'; // convert from ascii to numeric
Rules.State.Add(StateNode("usrscore",usrscore)); // (ctor turns the # to string)
// adjust by a percentage. rules don't win very often, and must adjust by large amounts
double payfactor = 1;
switch(usrscore)
{
case 0: payfactor = .25; break;
case 1: payfactor = .50; break;
case 2: payfactor = .75; break;
case 3: payfactor = 1.00; break;
case 4: payfactor = 1.25; break;
case 5: payfactor = 2.00; break;
}
// there's only 1 winner, and PayAllWinners will find it. If it was accidentally
// deleted, no problem; and it allows not having to record winner* anywhere.
Rules.PayAllWinners2(payfactor);
Rules.Sweep(1); // delete rules with zero bid
Rules.ZeroFlags(TRUE,FALSE,TRUE); // we're through with wasawinner and source
// if score was 0 or 1, get a better suggestion from user
if((usrscore < 2) && yesno("Do you want to tell me what I should have said"))
{
// this will have to allow creating a new Rule, maybe not involving a keyword at all
string t = CmRuleCreate();
cout << "PGM: Previously, you said:\n" << Rules.State.GetValue("s") << endl;
cout << "PGM: This time, my response is:\n" << t << endl;
// write to log now because continue; bypasses the writing of it later in loop
// this will be handled more cleanly when it's a rule.
ofstream("dialog.usr",ios::app) << "PGM(re-do): " << t << endl;
// restart loop, to get score for this output and pay just-used rule.
continue;
}
cout << to_upper(Rules.State.GetValue("usrname")) << ": "; // prompt user
while(!kbhit()) // times only the delay, not the typing time.
; // do some idle processing?
usrdelay = range(0L, 65535L, clock() - usrdelay);
Rules.State.Add(StateNode("usrdelay",(uint)usrdelay));
getline(cin,s,'\n'); // get the user's input sentence(s)
// it may lack final terminator: no getsentence call yet
PreprocessUserInput(s); // preprocess control chars,tabs,mult.spaces
// get old s and repost it as sprev (previous)
Rules.State.Add(StateNode("sprev",Rules.State.GetValue("s")));
// posting the raw s HERE makes user's entire input available to keyword search rules.
// but note that s IS preprocessed: contractions have been expanded.
Rules.State.Add(StateNode("s",s)); // post what user said as soon as we have it
ofstream("dialog.usr",ios::app) << // AND write it to log file.
to_upper(Rules.State.GetValue("usrname")) << ": " << s << endl;
//---------------------------------------------------------------------------
// commands to the program
//---------------------------------------------------------------------------
// make rules to handle these, anyway.
if(s == "bye") // program exit
return;
//---------------------------------------------------------------------------
// it would be ok to alternatively call respond(), then use GetValue("pgmlast").
string t = respond();
cout << "PGM: " << t << endl; // get and print response from "doctor"
} // while(1)
} // Converse
//----------------------------------------------------------------------------
// end class Talker
//////////////////////////////////////////////////////////////////////////////
int main()
{
randomize();
string::set_case_sensitive(0);
ifstream infile("reqfiles.dat"); // read list of all files program will need
if(!infile) // avoids getting partway along with files missing,
aborts("REQFILES.DAT not found."); // and avoids checking at the time a file is used.
BOOL missing = FALSE; // true if any file was missing
string filename;
string errortext;
while(infile >> filename) // make sure each file is there
{
// cout << "Looking for: " << filename << endl;
ifstream testopen(filename.c_str());
if(!testopen)
{
errortext += string("Required file ") + to_upper(filename) + " not found.\n";
missing = TRUE;
}
}
infile.close(); // the file we were reading filenames from
if(missing) // must quit if any file missing
{
cout << errortext;
return(1);
}
Talker t;
t.help();
t.Converse();
cout << "Bye." << endl;
return(0);
} //main
//////////////////////////////////////////////////////////////////////////////
|
|
|
|
|
|