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

Natural language processing chatbot program, Borland C++ ObjectWindows

WTalk.cpp is is the main program module for the WTalk.cpp natural language processing (NLP) chatbot project.

The program was written for Windows 3.1 in Borland C++ 4.0 using the ObjectWindows Library (OWL). It interacts with a Microsoft Access database.

There is substantially more information about this program on its home page. See link above.

/*	wtalk.cpp         	3/5/02
	This is part of the WTalk project.
	Copyright (C)1993-2002 Steven Whitney.
	Published under GNU GPL (General Public License) Version 2, with ABSOLUTELY NO WARRANTY.
	Initially published by http://25yearsofprogramming.com.

Win16 target.  An attempt at a program that acquires and uses knowledge
about the world to carry on an "interesting" conversation with user.
This version uses DDE to manage its database in WTALK.MDB.
All routines for the old internal dictionary have been removed.
Thus, this and other source files for this current version of the project no longer
contain code that was required by earlier versions.
That old code exists only in the old archive files.
DO NOT CONVERT TO WIN32 yet (potential Z-order problems, and maybe others).

5/19/2006. Regarding the next note: I don't remember program crashes being a particular problem.
I think this note was left over from earlier versions where the major concern was not that the program
WOULD crash, but that the consequences could be dire if it did: many objects were auto-written
to disk in their destructors, potentially corrupting the many text files. Therefore, if the
program ever hung up or appeared to be malfunctioning, this was a reminder that
the two methods described should be safer ways to terminate it manually, avoiding the
destructor calls. 
	"If program crashes, the computer's reset BUTTON is the only way to ensure
	NO disk writes, including from IDE, Windows, or my object destructors.
	It seems that ctl-alt-del (just once) Windows shutdown also bypasses destructors(?)."

------
To do:

pgm has no ability to handle possessives: thinks window's is a contraction.

look at oldtalk.c for how to investigate & answer user questions: who, didwhat, etc.
look at oldtalk.c (parse()) for how to make use of whenpreps, etc., and assign infotype
also need to extract subject, verb, direct object, indirect object.

each fact should have a member indicating who said it.

you shouldn't be constrained to reply to a specific user sentence.
use the last sentence, or few, for source material.

Eventually, program's state variables will need to also go into dialog.usr,
so it has a record of its own changing states.  Maybe screen all potential
additions for "ordinariness", omitting some.

create unused skeleton class Biology.cpp and h. for the simulated biology
(first determine whether its members are better simply as independent members of Talker.
i.e. should Biology really be a separate module?  What communications will it need
to and from the other classes?)

------
Notes:
--If you edit RULES.DAT manually, remember to revise rule count at top (2nd line).
--Most notes related to this program are now in talk.doc and wtalk.doc.
--Remember you can resize the window to watch MSAccess activity behind it.
--At 10/22/05, this program has about 6113 lines of code including headers and RC,
  plus 2798 lines (at least) from my libraries and class libraries = 8911 lines of code!

*/
// You MUST comment these out for any final version, since you don't want
// debug output generated if not running under debugger.
// #define __DEBUG	2
// #define __TRACE
// #define __WARN

#include <owl\owlpch.h>
#include <owl\opensave.h>
#include <owl\radiobut.h>
#include <owl\buttonga.h>
#include <owl\editsear.h>
#include <owl\statusba.h>
#include <owl\controlb.h>
#include <owl\inputdia.h>
#include <owl\validate.h>
#include <bwcc.h>					// IDHELP
#include <classlib\time.h>
#include <classlib\arrays.h>
#include <dir.h>                    // MAXPATH
#include "c:\bcs\my.h"
// #include "c:\bcs\library\wserial.h"
#include "c:\bcs\library\ddemlapp.h"
#include "c:\bcs\library\filearay.h"
#include "library.h"
#include "dic.h"
#include "fact.h"
#include "clasfier.h"
#include "dialogs.h"

#pragma hdrstop

#include "wtalk.rh"

//----------------------------------------------------------------------------

char AppName[] 	  	= "WTALK";
char INIFilename[]	= "WTALK.INI";    	// stored in c:\windows
string HelpFilename	= "WTALK.HLP";
string DATADir;								// (without trailing backslashes)

//////////////////////////////////////////////////////////////////////////////
// Conversation window
class STalkerWindow : public TLayoutWindow, public SDDEHandler
{
public:
	STalkerWindow(TWindow* parent = 0, const char* title = 0, DWORD instid = 0,
					const char* servicename = 0);
	~STalkerWindow();

	// functions
	// overridden base Window
	BOOL IdleAction(long idlecount);
	BOOL CanClose();
	void SetupWindow();

	// variables
	// static SWinCommDev SerialCom;	// for status reports, all share
						// SerialCom.Send(os.str());	// This is how you use it.
protected:
	// functions
	void log(const string& s);
	void ConvertIllegalChars(string& s); 	// convert to harmless
	void PreprocessUserInput(string& s);	//
	void sense();
	string respond();
	string ExecAction(const string& name);
	int SetVariable(const string& name, const string& value);
	virtual HDDEDATA DoDDEExecute(HSZ topic, HDDEDATA hData);

	// response subunit functions; each needs a line in ExecAction()
	// note there are also some ordinary fns that may be used as subunits
	string PostUserName();
	string askaboutpreviousstatement();
	string restateusersentence();
	string BuildCPIQuestion();
	string HandleAnswer();								// process a user yes or no answer
	string CallMDBFn(const string& fullcommandline);
	BOOL RunMDBMacro(const string& fullmacroname);
	string GetDDETransferValue(const string& varname);

	// event handlers
	void CmFileOpen();
	void CmFileExit()
		{
			oktoclose = TRUE;
			GetApplication()->GetMainWindow()->CloseWindow();
		}
	void CmRuleCreate();
	void CmRulesEdit();						// by id#
	void CmGetUserText(uint usrscore = 3);	// formerly Converse()
	void CmScore0() { CmGetUserText(0); }  	// these work fine, but sometime figure out
	void CmScore1() { CmGetUserText(1); }	// why I had trouble with COMMAND_AND_ID
	void CmScore2() { CmGetUserText(2); }	// for these, and abandoned it.
	void CmScore3() { CmGetUserText(3); }
	void CmScore4() { CmGetUserText(4); }
	void CmScore5() { CmGetUserText(5); }
	void CmHelpIndex();
	void CmHelpAbout();
	void CmDoNothing(){}
	void CmParseRevise(WPARAM cmd);		// revise and/or post TOK assignments to MDB

	// VARIABLES
	string Log;			// conversation record.  written to dialog.usr as it fills up.
	FactMemory Facts;
	RuleSet Rules;
	Rule* ruletoedit;	// default rule (if any) to be edited in CmRuleCreate
	BOOL oktoclose;		// to prevent accidental closing of the app by user pressing the ESC key.
	BOOL AppIsBusy;		// to prevent DDE transactions while real user is at console

	// pointers
	TStatic* parselabel; 		// static text labels
	TStatic* pgmlabel;
	TStatic* userlabel;
	TStatic* scorelabel;
	TEditSearch* parsewindow;	// the display windows
	TEdit* pgmwindow;
	TEdit* userwindow;
	TButton* score0;			// the pushbuttons at the bottom of the window
	TButton* score1;
	TButton* score2;
	TButton* score3;
	TButton* score4;
	TButton* score5;

	TFont* ParseWinFont;

	// 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

DECLARE_RESPONSE_TABLE(STalkerWindow);
};
//----------------------------------------------------------------------------
DEFINE_RESPONSE_TABLE1(STalkerWindow,TLayoutWindow)
	EV_COMMAND(CM_FILEOPEN1,CmFileOpen),
	EV_COMMAND(CM_FILEEXIT,CmFileExit),
	EV_COMMAND(CM_RULECREATE,CmRuleCreate),
	EV_COMMAND(CM_RULESEDIT,CmRulesEdit),
	EV_COMMAND(CM_HELPINDEX,CmHelpIndex),
	EV_COMMAND(CM_HELPABOUT,CmHelpAbout),
	EV_COMMAND(IDC_SCORE0,CmScore0),		// button responses...
	EV_COMMAND(IDC_SCORE1,CmScore1),
	EV_COMMAND(IDC_SCORE2,CmScore2),
	EV_COMMAND(IDC_SCORE3,CmScore3),
	EV_COMMAND(IDC_SCORE4,CmScore4),
	EV_COMMAND(IDC_SCORE5,CmScore5),
	EV_COMMAND(IDCANCEL,CmDoNothing),	// nope: app still closes on <ESC> pressed
	EV_COMMAND_AND_ID(CM_PARSEREVISE,CmParseRevise),
	EV_COMMAND_AND_ID(CM_PARSEISPERFECT,CmParseRevise),
END_RESPONSE_TABLE;
//----------------------------------------------------------------------------
// initialize static members
// SWinCommDev STalkerWindow::SerialCom;		// for status reports, all share

//----------------------------------------------------------------------------
// constructor
STalkerWindow::STalkerWindow(TWindow* parent, const char* title, DWORD instid,
								const char* servicename)
	: TLayoutWindow(parent, title), SDDEHandler(instid, servicename), Rules(500)
{
oktoclose = TRUE;		// in case anything goes wrong during construction
AppIsBusy = TRUE;

//---------------
parselabel = pgmlabel = userlabel = scorelabel = 0;
parsewindow = 0;
pgmwindow = userwindow = 0;
score0 = score1 = score2 = score3 = score4 = score5 = 0;
// ParseWinFont = new TFont("Courier New",FontHeightInPixels(10),0,0,0,FW_BOLD); // good
ParseWinFont = new TFont("FixedSys",FontHeightInPixels(9),0,0,0);

//-----------------------------------------
// Set up this window's startup attributes:
Attr.X = 1;
Attr.Y = 1;
Attr.W = 750;
Attr.H = 550;
// If layout window has a border, then it will automatically adjust children by 1 pixel
Attr.Style |= WS_BORDER;
// SetBkgndColor(TColor::Gray);

//--------------------
// initialize controls
// controls constructed in a TWindow have transfer disabled by default: OWLPG:237

// this TStatic MUST be just before the userwindow to use its underline as an accelerator
// use user's name for static label
string s = titlecase(Rules.State.GetValue("usrname")) + " (&you):";
userlabel = new TStatic(this, -1, s.c_str(), 8,205, 86,12, 0);

/* userwindow TEdit
	single-line allows TAB focus change, AND prevents ESC from closing the app,
	but absolutely will not wrap: 1 physical line only.
	multi-line allows auto-word-wrap, but disallows TAB focus change, and ESC closes the app!

	there is still one unresolved question:  the TEdit in the old AddWordDialog IS
	multiline, and TAB does go to the next control.  Is this a difference between
	how the KBHandler handles the keystroke and how Windows does when it has control
	of the dialog?  Sometime, experiment with a TEdit-derived multiline edit control
	with an overriding EvGetDlgCode that says it doesn't want tabs for itself,
	i.e. use for focus-change.  See edit.cpp.
	yikes!  Tab key VK_TAB is not the same as ^I, which goes into the TEdit.
	try ES_OEMCONVERT

	also searched to discover if a control's style is ever explicitly changed by owl
	or windows, thus inadvertently altering its behavior.
	see window.cpp line 2252: it does treat windows differently if from a resource.
*/
// TRUE/FALSE in next line is for multiline
userwindow = new TEdit(this,IDC_USERINPUT,"",8, 217, 363, 27, 0, FALSE);
userwindow->Attr.Style |= (WS_BORDER | WS_GROUP | WS_TABSTOP);
// userwindow->Attr.Style &= ~(ES_AUTOHSCROLL | WS_HSCROLL);// forces wordwrap in multiline
userwindow->Attr.Style |= (ES_AUTOHSCROLL);
// userwindow->Attr.Style |= (WS_HSCROLL);					// nonfunctional even if there
if(userwindow->Attr.Style & ES_MULTILINE)
	userwindow->Attr.Style |= (ES_WANTRETURN);			// they're stripped out later anyway
// userwindow->Attr.ExStyle |= (WS_EX_NOPARENTNOTIFY);	// crashes

scorelabel = new TStatic(this, -1,"Score:",8,251, 39,12, 80);

score3 = new TButton(this,IDC_SCORE3,"&3  Avg",161, 265, 36, 16, FALSE);
score3->Attr.Style |= (BS_LEFTTEXT | WS_GROUP | WS_TABSTOP);

score4 = new TButton(this,IDC_SCORE4,"&4  Good",212, 265, 36, 16, FALSE);
score4->Attr.Style |= (BS_LEFTTEXT | WS_GROUP | WS_TABSTOP);

score2 = new TButton(this,IDC_SCORE2,"&2  Poor",110, 265, 36, 16, FALSE);
score2->Attr.Style |= (BS_LEFTTEXT | WS_GROUP | WS_TABSTOP);

score5 = new TButton(this,IDC_SCORE5,"&5  Best",263, 265, 36, 16, FALSE);
score5->Attr.Style |= (BS_LEFTTEXT | WS_GROUP | WS_TABSTOP);

score1 = new TButton(this,IDC_SCORE1,"&1  Bad", 59, 265, 36, 16, FALSE);
score1->Attr.Style |= (BS_LEFTTEXT | WS_GROUP | WS_TABSTOP);

score0 = new TButton(this,IDC_SCORE0,"&0  Worst",  8, 265, 36, 16, FALSE);//FALSE=not default
score0->Attr.Style |= (BS_LEFTTEXT | WS_GROUP | WS_TABSTOP);

pgmlabel = new TStatic(this, -1,"Progra&m:", 8,150, 86,12, 20);

pgmwindow = new TEdit(this,IDC_PGMSAYS,"",8, 162, 363, 39, 0, TRUE);
pgmwindow->Attr.Style |= (WS_BORDER | WS_VSCROLL | WS_GROUP | WS_TABSTOP);
pgmwindow->Attr.Style &= ~(ES_AUTOHSCROLL | WS_HSCROLL);	// force wordwrap

parselabel = new TStatic(this, -1,"&Parse Result and Status Text:",8,  2,162,12, 80);

// If this works, the other TEdits could be TEditSearch
// esp. test userwindow: can a TEditSearch be single line?
// parsewindow = new TEdit(this,IDC_PARSEWINDOW,"",8, 14, 363, 133, 0, TRUE);
// #error the EditSearch window only accepts about 4800 chars via Insert().  why?
// Insert() may have a diff limit than for user entry.  try SetText()
// note that for TEdit you can specify TextLen in ctor, but TEditSearch uses 0.
parsewindow = new TEditSearch(this,IDC_PARSEWINDOW,"",8, 14, 363, 133);
parsewindow->Attr.Style |= (WS_BORDER | WS_GROUP | WS_TABSTOP);
parsewindow->Attr.Style |= (ES_AUTOHSCROLL | WS_HSCROLL);	// no wordwrap
parsewindow->Attr.Style |= (ES_AUTOVSCROLL | WS_VSCROLL);

//---------------------------------------------------
// set up child metrics (AFTER they all have values!)
// TStatics are better (less cramped) with height of 10.
// Scrollers are exactly 8 high or wide.
// If a TButton is too big to reDRAW it as DefButton, CR won't activate it.

TLayoutMetrics lm;
lm.X.Units = lm.Y.Units = lm.Width.Units = lm.Height.Units = lmLayoutUnits;

lm.X.Set(lmLeft, lmRightOf, lmParent, lmLeft, 8);
lm.Width.Set(lmRight, lmPercentOf, lmParent, lmRight, 50);
lm.Y.Set(lmTop, lmBelow, lmParent, lmTop, 0);
lm.Height.Absolute(10);
SetChildLayoutMetrics(*pgmlabel, lm);

lm.X.Set(lmLeft, lmRightOf, pgmlabel, lmRight, 0);
lm.Width.Set(lmRight, lmLeftOf, lmParent, lmRight, 8);
lm.Y.Set(lmTop, lmBelow, lmParent, lmTop, 0);
lm.Height.Absolute(10);
SetChildLayoutMetrics(*parselabel, lm);

// TButton controls are tied to window bottom
lm.X.Set(lmCenter, lmPercentOf, lmParent, lmRight, 700/12);
lm.Width.PercentOf(lmParent, 10);
lm.Y.Set(lmBottom, lmAbove, lmParent, lmBottom, 8);
lm.Height.Absolute(16);
SetChildLayoutMetrics(*score3, lm);

lm.X.Set(lmCenter, lmPercentOf, lmParent, lmRight, 900/12);
lm.Width.PercentOf(lmParent, 10);
lm.Y.Set(lmBottom, lmAbove, lmParent, lmBottom, 8);
lm.Height.Absolute(16);
SetChildLayoutMetrics(*score4, lm);

lm.X.Set(lmCenter, lmPercentOf, lmParent, lmRight, 500/12);
lm.Width.PercentOf(lmParent, 10);
lm.Y.Set(lmBottom, lmAbove, lmParent, lmBottom, 8);
lm.Height.Absolute(16);
SetChildLayoutMetrics(*score2, lm);

lm.X.Set(lmCenter, lmPercentOf, lmParent, lmRight, 1100/12);
lm.Width.PercentOf(lmParent, 10);
lm.Y.Set(lmBottom, lmAbove, lmParent, lmBottom, 8);
lm.Height.Absolute(16);
SetChildLayoutMetrics(*score5, lm);

lm.X.Set(lmCenter, lmPercentOf, lmParent, lmRight, 300/12);
lm.Width.PercentOf(lmParent, 10);
lm.Y.Set(lmBottom, lmAbove, lmParent, lmBottom, 8);
lm.Height.Absolute(16);
SetChildLayoutMetrics(*score1, lm);

lm.X.Set(lmCenter, lmPercentOf, lmParent, lmRight, 100/12);
lm.Width.PercentOf(lmParent, 10);
lm.Y.Set(lmBottom, lmAbove, lmParent, lmBottom, 8);
lm.Height.Absolute(16);
SetChildLayoutMetrics(*score0, lm);

lm.X.Set(lmLeft, lmRightOf, lmParent, lmLeft, 8);
lm.Width.Set(lmRight, lmLeftOf, lmParent, lmRight, 8);
lm.Y.Set(lmBottom, lmAbove, score0, lmTop, 0);
lm.Height.Absolute(10);
SetChildLayoutMetrics(*scorelabel, lm);

lm.X.Set(lmLeft, lmRightOf, lmParent, lmLeft, 8);
lm.Width.Set(lmRight, lmLeftOf, lmParent, lmRight, 8);
lm.Y.Set(lmBottom, lmAbove, scorelabel, lmTop, 0);
lm.Height.Absolute(16);						// single line
SetChildLayoutMetrics(*userwindow, lm);

lm.X.Set(lmLeft, lmRightOf, lmParent, lmLeft, 8);
lm.Width.Set(lmRight, lmPercentOf, lmParent, lmRight, 50);
lm.Y.Set(lmBottom, lmAbove, userwindow, lmTop, 0);
lm.Height.Absolute(10);
SetChildLayoutMetrics(*userlabel, lm);

lm.X.Set(lmLeft, lmRightOf, lmParent, lmLeft, 8);
lm.Width.Set(lmRight, lmPercentOf, lmParent, lmRight, 50);
lm.Y.Set(lmTop, lmBelow, pgmlabel, lmBottom, 0);
lm.Height.Set(lmBottom, lmAbove, userlabel, lmTop, 0);
SetChildLayoutMetrics(*pgmwindow, lm);

lm.X.Set(lmLeft, lmRightOf, pgmwindow, lmRight, 0);
lm.Width.Set(lmRight, lmLeftOf, lmParent, lmRight, 8);
lm.Y.Set(lmTop, lmBelow, parselabel, lmBottom, 0);
lm.Height.Set(lmBottom, lmAbove, userwindow, lmTop, 0);
SetChildLayoutMetrics(*parsewindow, lm);

//---------------------------
// initialize other variables
ruletoedit = 0;
// INITIALIZE UTILITY CLASS STATIC POINTERS TO THE SDDEHANDLER
FACT::Dde = TOK::Dde = (SDDEHandler*)this;

Rules.Load("rules.dat"); 	// requires that we are already logged into DATADir

AppIsBusy = FALSE;
oktoclose = FALSE;			// from now on, only File|Exit can close
}              				// constructor
//----------------------------------------------------------------------------
// destructor
STalkerWindow::~STalkerWindow()
{
ofstream("dialog.usr",ios::app) << Log << endl;
Rules.State.Remove("Log");			// so it's not read at next startup
delete ParseWinFont;

// you don't write DATADir to .ini because it never changes while running.
// you can only change it manually in the .ini file
}                       	//destructor
//----------------------------------------------------------------------------
// append a string to Log string, writing to dialog.usr as it becomes full.
// so that searchable Log contains up to the latest 64k chars of the conversation.
void STalkerWindow::log(const string& t)
{
if( ((long)Log.length() + t.length()) > 60000L)
{
	// the calc could be more exact than this
	ofstream("dialog.usr",ios::app) << string(Log,0,t.length()); // NO endl
	Log.remove(0, t.length());
}
Log += t + "\n";							// do append endl to the logged line
}   						//log
//----------------------------------------------------------------------------
// 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, ExecAction 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 STalkerWindow::ExecAction(const string& name)
{
// The order of some or all of these sections is important. If a most likely or appropriate
// test fails, then processing falls thru to the next possibility.

// TEST FOR ELEMENTAL SUBUNITS FIRST (they're common and the tests are fast)
if(name == "PostUserName") { PostUserName(); return "";  }
if(name == "askaboutpreviousstatement") { return askaboutpreviousstatement(); }
if(name == "restateusersentence") { return restateusersentence(); }
if(name == "ASKCPIQ") { return BuildCPIQuestion(); }
if(name == "HandleAnswer") { return HandleAnswer(); }

if(name == "CmFileExit") { CmFileExit(); return "Bye."; }
if(name == "CmFileOpen") { return "(CmFileOpen) Use File|Open to read a file."; }

// #error this ("toks") is now the last!: show next to last
// this should also go to the parsewindow
if(name == "toks")
{
	return Facts.ShowLast();
}
if(name == "showstate")
{
	ofstream("states.dat",ios::app) << endl << Rules.State << endl;
	return "Current state (can be quite long) was appended to file STATES.DAT";
}
if(name == "answerq") { return Facts.answerq(); }

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.
string value = Rules.State.GetValue(name);
if(value.length())
	return value;

// SEE IF THE VARIABLE IS STORED IN THE DDETRANSFERS TABLE; IF SO, RETURN IT.
value = GetDDETransferValue(name);
if(value.length())
	return value;

// SEE IF THE COMMAND IS AN ENGLISH VERB PHRASE WHOSE ACTION LIST CAN BE FOUND IN MDB.
// Since actions are carried out via recursive calls to this fn, an action list can
// contain anything executable BY this fn, including another English phrase that can
// in turn be looked up until the trail bottoms out in elemental units (or not).
//
// Currently, the *exact*, whole, command specified by the rule must be in MDB with an
// action list, but the eventual goal is to be able to parse the command, extract its
// pieces & info, and search MDB for a best match of the command's *intent* (even using
// synonym search) so that if an exact command isn't found, it can find and execute
// the most similar one that does have an action list.  Then a command can originate
// anywhere (such as from user, OR self-generated), and needn't be exact.
//
string topic("WTALK;SQL SELECT DISTINCTROW Defs.OpDef "
	"FROM Phrases INNER JOIN Defs ON Phrases.AlphaID = Defs.AlphaID "
	"WHERE ((Phrases.Alpha=\"%s\") AND (Defs.Type=2) AND (Defs.OpDef Is Not Null));" );
topic.substring("%s") = name;
SDDEConv* chan1 = DDEInitiate("MSACCESS",topic);
if(chan1)
{
	// for now, use the first def found.  later, read all, and use context to determine best.
	if(chan1->DDERequest("NextRow"))
	{
		string t = (string)(*chan1);    // the raw OpDef, possibly with embedded CRLF
		t = unqstring(t);         				// remove surrounding quotes, if any
		t = CRLFtoLF(t);                        // change CRLF to LF only
		t += "\n";      						// ensure getline() works properly

		string cmd;      						// 1 action list line
		string response;						// for building return value
		char* opdef = strnewdup(t.c_str());
		istrstream is(opdef);
		while(getline(is, cmd, '\n'))
		{
			// if a phrase has itself in its own action list, pgm would crash: stack overflow
			// (can also occur if any loop circuit exists - should be rare)
			if(cmd.length() && (cmd != name))
				response += ExecAction(cmd) + " ";
		}
		delete[] opdef;
		DDETerminate(chan1);
		return(response);  	// an OpDef was found; return response no matter what it contains
	}
	DDETerminate(chan1);
}
// SEE IF NAME IS THE NAME OF A MACRO TO RUN; IF SO, RUN IT.
// COULD BE RISKY: MACROS CAN ALTER AND POSSIBLY DAMAGE THE DATABASE, AND
// SOME ARE NOT INTENDED TO BE ACCESSIBLE AT RUNTIME.
if(RunMDBMacro(name))
	return "";
// 	return(string("I ran the macro: ") + name);    	// for debugging

// NAME WAS UNKNOWN, SO LAST RESORT IS TO INTERPRET IT AS TEXT FOR LITERAL OUTPUT
return name;
}                      		//ExecAction
//----------------------------------------------------------------------------
// 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 STalkerWindow::SetVariable(const string& /* name */ , const string& /* value */ )
{
// if(name == "var1") { fromstring(var1,value); return(TRUE); }
return(FALSE);
}                     		//SetVariable
//---------------------------------------------------------------------------
/*
	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 ExecAction().  If it returns anything besides string,
	   you can use tostring() in ExecAction 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 from last user input, 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.
//
// Program should be able to recognize when it has asked a question, and know what
// to do with replies.
//
// It's a way for getting and making USABLE the user's answers to questions the program ASKS.
// This is also a natural location to check the retrieved input against what
// is reasonable as input in the current situation/context.  (Validation:) I.e.
// 1) Does the low-level format of this input match what is reasonable?
//    (number of words, number of chars, types of chars alpha/digits).  If so,
// 2) Does the information parsed from this input fall within the range of what is
//    known to be reasonable for what was being sought?
//    (e.g. is the retrieved name a recognized human name?)
// 3) Do I already have an entry for this information, and if so, does the new
//    information confirm or conflict with it?  (StateVar and/or HWK lookup).
// HWK = Hierarchical World Knowledge, a database organization strategy discussed in talk.doc.
string STalkerWindow::PostUserName()
{
string usrname = Rules.State.GetValue("s");
if(usrname.length())
{
	FACT name(usrname);

	// strip off such phrases as "my name is"
	FACT noise("my name is.");
	name.Destroy(noise);

	usrname = name.RebuildSentence();
	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 STalkerWindow::askaboutpreviousstatement()
{
static int factpos = 0;		// #error: note as static it may be shared by all STalkerWindows
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 STalkerWindow::restateusersentence()
{
string response = "Please continue."; 	// last resort
uint i = Facts.GetItemsInContainer();
// #error use tolower() on the fact
if(i)
	response.prepend("You said " + Facts[i - 1]->ReversedContext() + "  ");
return(response);
}                      		//restateusersentence
//--------------------------------------------------------------------------
// get a CPI question from mdb and reformat it for presentation.  previously,
// each CPI question had 2 rules in rules.dat dedicated to it, resulting in about 960
// rules handling this trivial function, which was only a novelty to start with.
// now, only 2 rules cause this fn to be called, which calls MDB to randomly select one
// question and return it.  The minor loss is that the *individual* questions lose their
// identity and ability to receive feedback.
string STalkerWindow::BuildCPIQuestion()
{
string response;

SDDEConv* chan1 = DDEInitiate("MSACCESS","WTALK");
if(chan1)
{
	chan1->DDEExecute("[CPIQuestionManagement.MarkRandomCPIQuestion]");
	DDETerminate(chan1);

	string t("WTALK;SQL SELECT DISTINCTROW CPIQuestions.Question FROM CPIQuestions "
			 "WHERE ((CPIQuestions.Marked=Yes));");
	chan1 = DDEInitiate("MSACCESS",t);
	if(chan1)
	{
		if(chan1->DDERequest("NextRow"))
			response = (string)(*chan1);
		DDETerminate(chan1);
	}
}
response.prepend("If I said, \"");
response += "\", would you say that this statement applies to you?";
Rules.State.Add(StateNode("CPIQPENDING","TRUE"));	// flag that there is a CPIQ pending
return(response);
}                      		//BuildCPIQuestion
//----------------------------------------------------------------------------
// apply a user reply to whatever question is pending.
string STalkerWindow::HandleAnswer()
{
string response("I don't remember asking a question!");

// IF THERE IS A CPI QUESTION PENDING, POST ITS ANSWER TO MDB
if(Rules.State.GetValue("CPIQPENDING").length())
{
	SDDEConv* chan1 = DDEInitiate("MSACCESS","WTALK");
	if(chan1)
	{
		string tf("U");   			// in case of error, U results in no change
		string value = Rules.State.GetValue("sentype");
		if(value == "00003")		tf = "T";
		else if(value == "00004")   tf = "F";
		string t("[SetWarnings 0]"
				 "[RUNSQL \"UPDATE DISTINCTROW CPIQuestions "
				 "SET CPIQuestions.Answer = \"\"%s\"\" "
				 "WHERE ((CPIQuestions.Marked=Yes));\"]"
				 "[SetWarnings -1]");
		t.substring("%s") = tf;
		chan1->DDEExecute(t);
		DDETerminate(chan1);
	}
	Rules.State.Remove("CPIQPENDING");		// no longer pending
	response = "Thank you for answering.";
}
// #error other answer handlers go here

return(response);
}                      		//HandleAnswer
//--------------------------------------------------------------------------
// calls a built-in or user-defined MSAccess function from an MDB module.
// fullcommandline must consist of the entire text required to call the fn,
// including (), whether containing args or empty.
// returns whatever that fn returns, as a string.
// NOTE: THERE IS A 255 CHAR LIMIT ON THE LENGTH OF A STRING CREATED AND RETRIEVED
// IN THIS MANNER.  LONGER STRINGS ARE AT LEAST TRUNCATED, AND COULD BE UNSAFE.
// FROM BRIEF TESTING, IT SHOULD PROBABLY BE SAFE IF YOU KNOW THE RETURN
// VALUE LENGTH WILL BE LESS.  OTHERWISE, FIRST POST THE DESIRED STRING TO A TABLE AND USE
// NORMAL MEANS TO RETRIEVE IT FROM THE TABLE; THIS WILL CAUSE IT TO BE RECOGNIZED AS
// A MEMO TYPE, AND ENCLOSED IN QUOTES (WHICH YOU MUST REMOVE) AND TRANSFERRED PROPERLY.
string STalkerWindow::CallMDBFn(const string& fullcommandline)
{
string response;

// the DDETransfers table is used as a dummy; no actual data is retrieved from it.
// to work, it must (and always does) contain at least 1 record.
string topic("WTALK;SQL SELECT TOP 1 %s AS Expr1 FROM DDETransfers;");
topic.substring("%s") = fullcommandline;
SDDEConv* chan1 = DDEInitiate("MSACCESS",topic);
if(chan1)
{
	if(chan1->DDERequest("NextRow"))
		response = (string)(*chan1);
	DDETerminate(chan1);
}
return(response);
}                      		//CallMDBFn
//--------------------------------------------------------------------------
// calls an MSAccess macro in WTALK.MDB.  fullmacroname must be either a top level macro
// name as shown in the Macros window, or the fully-qualified name of a macro in a macro group.
// Omit the enclosing brackets, which this fn automatically adds.
// returns TRUE if the macro was found and run, else FALSE.
BOOL STalkerWindow::RunMDBMacro(const string& fullmacroname)
{
BOOL result = FALSE;
SDDEConv* chan1 = DDEInitiate("MSACCESS","WTALK");
if(chan1)
{
	if(chan1->DDEExecute(bstring(fullmacroname)))
		result = TRUE;
	DDETerminate(chan1);
}
return(result);
}                      		//RunMDBMacro
//--------------------------------------------------------------------------
// retrieves the value of a user-defined variable from the DDETransfers MDB table.
// returns the value as a string, or an empty string if not found or connection fails
// or the value actually IS null.  (no way to distinguish)
string STalkerWindow::GetDDETransferValue(const string& varname)
{
string value;
string topic("WTALK;SQL SELECT DISTINCTROW DDETransfers.Value FROM DDETransfers "
			 "WHERE ((DDETransfers.VarName=\"%s\"));");
topic.substring("%s") = varname;
SDDEConv* chan1 = DDEInitiate("MSACCESS",topic);
if(chan1)
{
	if(chan1->DDERequest("NextRow"))
		value = (string)(*chan1);
	DDETerminate(chan1);
}
return(value);
}                      		//GetDDETransferValue
//---------------------------------------------------------------------------
// END OF RESPONSE SUBUNIT FUNCTION SECTION
//--------------------------------------------------------------------------
// 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 provision 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 STalkerWindow::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);

// clear here in sense() because it's called by both CmGetUserText AND CmFileOpen
parsewindow->SetText("");	// enable if you don't want text to accumulate

// 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();
uint slenwords = 0;
for(int i = factcount - newfacts ; i < factcount ; i++)
{
	slenwords += Facts[i]->GetItemsInContainer();
	append(parsewindow, Facts[i]->ShowToks());		// show parsing of the new facts
}
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");

	// THIS IS THE FIRST LOCATION (FN) IN WTALK AFTER RECENT FACTS WERE JUST ADDED.
	// IT IS A STRANGE LOC FOR THESE FOLLOWING TESTS, BUT MAY BE A GOOD ONE.
	// reuse lastfact.  now it is the FIRST new fact, to test for being an answer.
	// (that part not yet written!)
	lastfact = Facts[factcount - newfacts];

	// also to do here? for any of the just-added facts,
	// if an entire FACT was marked WHY, it's probably a partial sentence, probably pertaining
	// to previous fact, or answers last question pgm asked user.  If so, determine which,
	// and insert or append this FACT where appropriate.

	// (try pronoun resolution here?)
	// if(Pronouns.HasMember(toks[i]))
	// #error Add tests for "it was", etc., such as "it was warm", "it was late", "it was cloudy"?

}					// if(newfacts)
else
{
}
Rules.State.Add(StateNode("Log",Log));	// rules can search for things said this session

// post late because pgm itself can now add facts
Rules.State.Add(StateNode("factcount",Facts.GetItemsInContainer()));
}							//sense
//----------------------------------------------------------------------------
// RESPOND() create the doctor's responses
string STalkerWindow::respond()
{
string response;                        				// function's return value

// if you enable this loop, pgm could carry out actions until one does generate a response,
// which would allow it to do some things other than respond to user (idle processing).
// a drawback is that the interim actions won't get any direct feedback.
// (I've done no checking whether this loop would work without mods inside it.)
// while(!response.length()) {

// 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.
Rules.Sweep(1);					// a good place to delete rules with zero bid
// this can fail if there are no rules, which occurs if wrong dir was logged at pgm startup.
// so this must be "if" until the method of adding new rules is actually implemented.
if(!Rules.MarkBidders() && Rules.State.GetItemsInContainer())
// while(!Rules.MarkBidders() && Rules.State.GetItemsInContainer())
{
	logerror("MarkBidders failed.");
	// if there were no bidders, add some new Rules,
	// one tailored to respond to each of the current StateArray states?
	// but 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();
}
//----------
ostrstream os;
uint biddercount = 0;
os << "Bidders: " << endl;
for(uint m = 0 ; m < Rules.GetItemsInContainer() ; m++)
	if(Rules[m]->isabidder)
	{
		os  << setw(5)
			<< Rules[m]->id; //  << " ";
		if(!(++biddercount % 10))
			os << endl;
	}
os << 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.
{
	os << "Winner: " << winner->id << ", bid(" << winner->bid << ")" << endl;
	os << "Pays 1% to #" << (prevwinner ? prevwinner->id : 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
}
os << "===================================" << endl;
os << ends;
append(parsewindow, os.str());
delete[] os.str();

// 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
// for now, multiple actions just extend the output string.
if(winner)															// just in case
	for(int i = 0 ; i <  winner->Actions.GetItemsInContainer() ; i++)
		response += ExecAction(winner->Actions[i]) + " ";
else
	response += "Error: no winning rule; no response.";

// }	// end while(!response.length())

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
log("PGM: " + response);							// AND write to log

// 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
//--------------------------------------------------------------------------
// replace any chars in user input that will cause problems anywhere in MDB or program.
//
// easier just to do it here than make accomodations all over the place, such as this:
// (embedded " and | make SQL stmts invalid)
// SELECT * FROM Phrases WHERE ((Phrases.Alpha=Chr$(34))) OR ((Phrases.Alpha=Chr$(124)));
//
void STalkerWindow::ConvertIllegalChars(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 Log or states.dat.
			if(s[i] == ']')
				s[i] = ')';		// Can change to parentheses or curly braces.
			else
				if(s[i] == '|')			// vertical bar confuses SQL
					s[i] = '/';
				else
					if(s[i] == '"')		// quotes confuse SQL, change to ^
						s[i] = '^';     // this change hasn't been accomodated thruout pgm
										// unsure what effects it might have
}
}   						//ConvertIllegalChars
//--------------------------------------------------------------------------
// preprocess an incoming string to put it into a standardized format.
void STalkerWindow::PreprocessUserInput(string& s)
{
if(!s.length())
	return;

ConvertIllegalChars(s);

// 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
		char buf[80];
		string prompt =
			"This word contains an apostrophe. If it is a contraction, "
			"please enter the words that it expands to, using lower case "
			"except for any word that SHOULD always be capitalized.  "
			"If the word is not a contraction, choose OK to leave it as-is."
			"What does " + to_upper(token) + " mean?";
		string t;
		while(!t.length())					// only an empty expansion is illegal
		{
			token.copy(buf,sizeof(buf));	// set up token as its own default expansion
			while(TInputDialog(this,"Contraction Expansion",prompt.c_str(),
								buf,sizeof(buf)).Execute() != IDOK)
				;				// require IDOK, not IDCANCEL
			t = buf;
		}
		s2 += t + " "; 	   		// add original token or expansion text (whichever)
		if(t != token)			// if different, add new contraction to ContractionList
			FACT::Contractions.Add(TextSub(token,t));
	}                        	// end while(there are tokens to process)
	s = s2;						// s2 contains all expansions made, if any
}								// if(s.contains("'"))
ConvertIllegalChars(s);			// just in case any more got in.

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()

// consider adding this somewhere:
// (put note in dialog about revising capitalization, if appropriate.)
// on exit, revise alpha, if user changed only the capitalization.
// 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)		// old member
// 		if(toks[0].lookup->HasType(NOUN))
// 			if(isupper(*(toks[0].lookup)[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);
// 			}

// ASK USER ABOUT UNKNOWN WORDS AND ADD THEM TO DIC
// it is now done in TOK ctor so it gets done for any new word, however acquired

// consider re-posting S here in expanded form.  This seems a good idea,
// but some existing rules have contractions and should be changed first.
// Rules.State.Add(StateNode("s",s));
}               		//PreprocessUserInput
//--------------------------------------------------------------------------
// read sentences from file as though they came from user.  no rule scoring is done.
// You must not allow this to be called from respond(): i.e. it cannot be a subunit fn.
void STalkerWindow::CmFileOpen()
{
// checked: ok
TOpenSaveDialog::TData filedata(OFN_HIDEREADONLY | OFN_FILEMUSTEXIST | OFN_NOCHANGEDIR,
								 "Text Files (*.txt)|*.txt|",0,0,"TXT");
if((TFileOpenDialog(this,filedata)).Execute() != IDOK)
{
	if(filedata.Error)
	{
		string s = string("Error code: ") + tostring(filedata.Error);
		MessageBox(s.c_str(),"Dialog box error",MB_OK | MB_ICONEXCLAMATION);
	}
	return;
}
// show "wait" cursor through entire file read
HCURSOR oldcursor = ::SetCursor(::LoadCursor(NULL, IDC_WAIT));
ifstream infile(filedata.FileName);
string s;
while(getsentence(infile,s))				// getsentence may add a terminator
{
	PreprocessUserInput(s);					// preprocess control chars,tabs,mult.spaces
	append(parsewindow, s);					// display line from file
	Rules.State.Add(StateNode("s",s));		// post it, and write it to log file
	log(to_upper(Rules.State.GetValue("usrname")) +
			"(" + filedata.FileName + "): " + s);
	// if you DON'T call respond(), you MUST call sense() so facts go into Facts.
	// sense();
	// don't call CmFileOpen FROM respond:
	// lines in the file would cause recursive entry to respond() and thus duplicate sense().
	// still unsure how much of a disaster that would be.  All the sense() calls
	// for file lines would overwrite the original sense() that responded to
	// the user's command "readfile".  But by that time, the winning rule had
	// been determined, and respond() was just calling ExecAction on the action list.
	// so it's the final stages of respond() that would cause problems, if any,
	// and it may be that the result of recursive entry would be exactly what you'd want:
	// as though input from the file came from user.
	pgmwindow->SetText("");
	append(pgmwindow, respond()); 			// get and print response from "doctor"
}
::SetCursor(oldcursor);  					// restore cursor
}                      		//CmFileOpen
//----------------------------------------------------------------------------
// get score and input text from user's input window.
void STalkerWindow::CmGetUserText(uint usrscore)
{
static TTime start;						// times intervals between user responses
HCURSOR oldcursor = ::SetCursor(::LoadCursor(NULL, IDC_WAIT));

// in this version, the time is in seconds, and has to include user's typing time.
ClockTy usrdelay = range((ClockTy)0, (ClockTy)MAXUINT, TTime().Seconds() - start.Seconds());
Rules.State.Add(StateNode("usrdelay",(uint)usrdelay));

// rules don't win very often, and must adjust quickly (by large amounts)
double payfactor = 1;		// payment: adjust bid by a percentage
switch(usrscore)           	// user's rating of pgm reply, set by which button was pressed
{
	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;
	default: payfactor = 1.00; logerror("illegal user score = " + tostring(usrscore)); break;
}
Rules.State.Add(StateNode("usrscore",usrscore));

// 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);

// now you probably could bring up the Rule Editor if score was 0 or 1, but don't, for now.
// I think you'd do it here.

int bufsize = userwindow->GetTextLen() + 1;
char* buf = new char[bufsize];			// user's input text
userwindow->GetText(buf,bufsize);
string s = buf;							// get the user's input sentence(s)
delete[] buf;

PreprocessUserInput(s);			// preprocess control chars(incl.CRLF),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
										// AND write to dialog file
log(to_upper(Rules.State.GetValue("usrname")) + ": " + s);

// commands to the program were here

string t;
// pgmwin should just accumulate text (as a reference log for user)
// accumulate up to 30,000 chars, then empty and restart.
// t = s + "\n\n";
// it would be ok to alternatively call respond(), then use GetValue("pgmlast").
t = respond();							// get response from "doctor"
pgmwindow->SetText("");
append(pgmwindow, t);

append(parsewindow, "Last score was: " + tostring(usrscore));

start = TTime();						// restart timer

// restore before setting focus: TEdit may (should) restore it to caret, anyway.
::SetCursor(oldcursor);

// update static label with user's name
// #error you should retrieve this from MDB, where it is the OpDef of "user"
s = titlecase(Rules.State.GetValue("usrname")) + " (&you):";
userlabel->SetText(s.c_str());

// userwindow->Clear();	// empty user's TEdit for next reply: no, next input will replace it.
// works, but when you exit the app, it leaves no window in previous app with the input focus
// if still true, maybe could set focus to one of the buttons just before exit
userwindow->SetFocus();
userwindow->SetSelection(0,-1);
} 							// CmGetUserText
//---------------------------------------------------------------------------
// user creates a new Rule.
// 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 simply use *its* language to show it how.
void STalkerWindow::CmRuleCreate()
{
// create a rule based on user's previous reply, mainly so we can display it in
// the editor as an example of the required format, and to make it easier to
// create a keyword-responder rule, the easiest type for the user to create.
Rule r;														// it gets next available id#
if(ruletoedit) 		// if we came from CmRulesEdit and know the id, use that rule
	r = *ruletoedit;
else                // else create a default rule
{
	r.IfNodes.Add(StateNode("S",Rules.State.GetValue("s"),7));  // an IfNode for S
	r.Actions.Add("Action");									// dummy does nothing
	r.bid = 100;
	r.locked = TRUE;										// usu. want user's rule locked
}
// regardless of how we got it, the EDITED rule wasn't a winner,
// and it is NOT used immediately as a re-do, either.
r.wasawinner = FALSE;
TranStruct* tts = new TranStruct;
while(1)					// keep trying until rule is valid OR user cancels
{
	// first write into filechars as a temp buf
	ostrstream(tts->filechars,sizeof(tts->filechars)) << r << ends;
	// now change to TEdit format and write it back in again
	LFtoCRLF(string(tts->filechars)).copy(tts->filechars,sizeof(tts->filechars));
	if(SRuleEditDialog(this,tts).Execute() != IDOK)
		break;
	Rule r1;
	istrstream is(tts->filechars);
	if(is >> r1)		// read only into a temp Rule, and do nothing if read failed
	{
		r = r1;    					// if read succeeded, your new rule becomes new default
		// if the rule ID# is not a dup, Add it unconditionally.
		if(Rules.Find(r.id) == 0)
		{
			Rules.Add(r);
			break;          		// and quit.
		}
		// the rule's ID was found: we either came from CmRulesEdit,
		// or user changed the id# to a dup.
		// if we came from CmRulesEdit AND id# was not changed, write is always ok.
		if(ruletoedit && (ruletoedit->id == r.id))
		{
			*ruletoedit = r; 		// replace its contents in-place,
			break;          		// and quit.
		}
		// if it falls through, user changed the id# TO a dup,
		// handled below, and loop to retry.
	}
	// for now, if rule (r1) could not be read, you lose it (you lose its text).
	// restart loop with last good version (r).
	MessageBox("ID# a duplicate?  Revise rule and retry.","Edited Rule was Invalid",
				MB_OK | MB_ICONEXCLAMATION);
}
delete tts;
}                	 	   //CmRuleCreate
//---------------------------------------------------------------------------
// get the ID# of a rule to edit, for CmRuleCreate
void STalkerWindow::CmRulesEdit()
{
ulong idno = 0;						// start with 0 (nonexistent), in case no winner found
char buf[20] = "0";                 // text to match
ruletoedit = Rules.Winner();		// default is the latest winner, 0 if none found
if(ruletoedit)
	idno = ruletoedit->id;
ostrstream(buf,sizeof(buf)) << idno << ends;
while(1)
{
	if(TInputDialog(this,"Rule to View or Edit","Enter the ID# of the rule to view/edit:",
						buf,sizeof(buf)).Execute() != IDOK)
		break;
	istrstream(buf) >> idno;
	if((ruletoedit = Rules.Find(idno)) != 0)
		CmRuleCreate();
	else
		MessageBox("Rule ID# entered was not found in Rules.","Rule Not Found",
					MB_OK | MB_ICONEXCLAMATION);
}
ruletoedit = 0;
}                	 	   //CmRulesEdit
//----------------------------------------------------------------------------
// upload TOKs from last FACT to MDB, allow optional user editing, and update MDB.
void STalkerWindow::CmParseRevise(WPARAM cmd)
{
uint factcount = Facts.GetItemsInContainer();
if(!factcount || !Facts[factcount - 1]->UploadToTOKSTable())
	return;

SDDEConv* chan1 = DDEInitiate("MSACCESS","WTALK");
if(!chan1)
	return;
chan1->DDEExecute("[OpenForm ToksEditor][SetWarnings -1][Maximize]");
if(cmd == CM_PARSEREVISE) 				// if user wanted to EDIT, task switch
{
	MessageBox("Carefully revise the form fields for each word shown.  "
			   "Use Alt+TAB or Ctrl+ESC to return here and close this box when you are done.",
			   "Please Task-Switch To MSAccess *NOW* To Revise Info",
													MB_OK | MB_ICONINFORMATION);
}
chan1->DDEExecute("[Close 2,ToksEditor]");
// actions making use of the revised (or already-perfect) info are in the Form's OnClose
// event handler, and thus were activated just by opening and closing it,
// regardless of whether user edited.(NOTE: DEFAULT VALUE OF UPDATEONCLOSE CONTROL MUST BE TRUE)
// (don't bother reloading the FACT here.  User can just re-enter it, if desired.)

// #error For now, this is the only place in pgm where you can have any confidence at all
// that a FACT has reliable info, so this is where you would extract descriptions,
// equivalencies, attributes, etc., and post them to MDB as links.
// But call a FACT member fn that does the real work (?).

DDETerminate(chan1);
}                    		//CmParseRevise
//----------------------------------------------------------------------------
void STalkerWindow::CmHelpIndex() { WinHelp(HelpFilename.c_str(),HELP_INDEX,0); }
//----------------------------------------------------------------------------
void STalkerWindow::CmHelpAbout()
{ MessageBox("Copyright (C)1993-2002 Steven Whitney",GetApplication()->GetName(),
			MB_OK | MB_ICONINFORMATION);
}
//----------------------------------------------------------------------------
// overridden TWindow virtuals
//----------------------------------------------------------------------------
void STalkerWindow::SetupWindow()
{
TLayoutWindow::SetupWindow();

//---------------
// setup
backupfile("dialog.usr");
Rules.State.Add(StateNode("usrname","USER"));	// default name for user
// maybe post some other statevars to reset them

//--------------------
// initialize controls
// set parsewindow to Courier New 10 Bold,  or fixedsys? (must be monospaced)
// can set up the others however you want.
parsewindow->SetWindowFont(*ParseWinFont,TRUE);

}							//SetupWindow
//----------------------------------------------------------------------------
BOOL STalkerWindow::CanClose()
{
if(!oktoclose)
{
	// message during testing only: normally you just don't close
	MessageBox("Use File|Exit to Exit",GetApplication()->GetName(), MB_OK | MB_ICONINFORMATION);
	return(FALSE);
}
// projects share error log (wtalk.err), so report only once from global location:
uint err = logerror();
if(err)
{
	string s = string("There were ") + tostring(err) + " errors.  See ERROR.LOG.";
	MessageBox(s.c_str(),GetApplication()->GetName(), MB_OK | MB_ICONINFORMATION);
}
return(TLayoutWindow::CanClose());
}                      		//CanClose
//----------------------------------------------------------------------------
BOOL STalkerWindow::IdleAction(long idlecount)
{
TLayoutWindow::IdleAction(idlecount);

if(!Rules.IsOK())			// if rules.dat was corrupt, report error and quit.
	CmFileExit();

// if main window (app) is iconic, are all owned windows? even if not, the MDIChild host
// could be even if the app is not, so must test it, too.
// if(GetApplication()->GetMainWindow()->IsIconic() || Parent->IsIconic())
// {
// }
return(TRUE);
}                        	// IdleAction
//----------------------------------------------------------------------------
// DDE fns
//----------------------------------------------------------------------------
// for simplicity:
// the starting text must be the command (multiple words ok);
// whatever text follows the command is the data, not enclosed in quotes (or anything);
// commands are NOT enclosed in [], and only 1 command is allowed per call.
#pragma argsused
HDDEDATA STalkerWindow::DoDDEExecute(HSZ topic, HDDEDATA hData)
{
HDDEDATA result = (HDDEDATA)DDE_FNOTPROCESSED;
if(AppIsBusy)				// reminder to refuse processing if another task is in progress.
	result = (HDDEDATA)DDE_FBUSY;
else
{
	string cmd;
	if(hData)
	{
		// cmd=(const char far*)hData: no error, but doesn't work.  global data inaccessible?
		DWORD datasize = DdeGetData(hData, 0, 0, 0); 	// get size of the buffer we need
		if(datasize)
		{
			char HUGE* Data = new HUGE char[datasize + 2];
			Data[0] = 0;								// preinitialize to null, just in case.
			if(!DdeGetData(hData, (uchar HUGE*)Data, datasize + 1, 0))
				DDEError(InstId,"DoDDEExecute: DdeGetData error");
			cmd = (const char far*)Data;
			delete[] Data;
		}
		if(cmd.length())
		{
			// TESTS GO HERE FOR WHATEVER COMMANDS YOU WANT TO SUPPORT
			// parse the provided text and leave the result in the MDB Toks table;
			// doesn't break the text into sentences,
			// so be sure there's only 1 sentence or phrase;
			// does not add to Facts; does not respond.
			if(cmd.substr(0,6) == "parse ")
			{
				cmd.remove(0,6);
				PreprocessUserInput(cmd);	 // preprocess control chars,tabs,mult.spaces
				FACT f(cmd,TRUE);
				f.UploadToTOKSTable();		 // if f is bad (empty), the table is emptied.
				result = (HDDEDATA)DDE_FACK; // must set result for each command handled
			}
		}
	}
}
if(!DdeFreeDataHandle(hData))      					// required cleanup
	DDEError(InstId,"DoDDEExecute");
return(result);
}               			//DoDDEExecute
//----------------------------------------------------------------------------
// 						end class STalkerWindow
//////////////////////////////////////////////////////////////////////////////
class TMyApp : public SDDEApplication
{
public:
	TMyApp(const char far *title, DDESides ddeside = CLIENT, const char* servicename = 0);

protected:
	virtual BOOL IdleAction(long idlecount);
	virtual void InitInstance();
	virtual void InitMainWindow();
	virtual int TermInstance(int status);

// 	void EvDropFiles(TDropInfo dropInfo);

	HWND MSAccessAtStartup;

DECLARE_RESPONSE_TABLE(TMyApp);
};
//----------------------------------------------------------------------------
DEFINE_RESPONSE_TABLE1(TMyApp,SDDEApplication)
// 	EV_WM_DROPFILES,
END_RESPONSE_TABLE;
//----------------------------------------------------------------------------
// constructor.  title only used in case of error
TMyApp::TMyApp(const char far *title, DDESides ddeside, const char* servicename)
	: SDDEApplication(title, ddeside, servicename)
{
MSAccessAtStartup = 0;  		// a value in case of startup abort
}								//constructor
//----------------------------------------------------------------------------
BOOL TMyApp::IdleAction(long /* idlecount */)
{
SDDEApplication::IdleAction(0);
return TRUE;
}
//----------------------------------------------------------------------------
void TMyApp::InitInstance()
{
// #error see if there is a way to prevent a 2nd instance from running.

// LOAD DATADIR FROM THE APP'S INI FILE.  I do this even before the base class
// InitInstance so that any files referenced in STalkerWin ctor or SetupWindow are
// read from the correct directory.
char buf[MAXPATH];      			// item from .INI file
getcwd(buf,MAXPATH);	      		// temp use to get current logged dir
string cwd = buf;					// copy it to use as the default parameter
GetPrivateProfileString("info","datadir",cwd.c_str(),buf,MAXPATH,INIFilename);
DATADir = buf;
FilePathParser(DATADir + "\\").ChDir();	// log into DATADir, changing drive if it's wrong
HelpFilename.prepend(DATADir + "\\");	// full path for WinHelp

//--------------------
// calls InitMainWindow, and installs Handler in list.
SDDEApplication::InitInstance();

//--------------------
// Make sure all files that the program will need are in the now-logged dir.
// Do it here, once, to avoid getting partway along with files missing,
// and avoids having to check and report failure when each file is read.
BOOL missing = FALSE;                  		// true if any file was missing
ifstream infile("reqfiles.dat");
if(infile)
{
	string filename;
	while(infile >> filename)              	// make sure each file is there
	{
		ifstream testopen(filename.c_str());
		if(!testopen)
		{
			logerror("Required file " + to_upper(filename) + " not found.\n");
			missing = TRUE;
		}
	}
}
else
{
	logerror("Required file REQFILES.DAT not found.");
	missing = TRUE;
}
infile.close();                         // the file we were reading filenames from
if(missing)          					// must quit if any file missing
{
	::MessageBox(GetFocus(),"Application cannot start. Data file(s) missing. See ERROR.LOG.",
					GetName(), MB_ICONSTOP|MB_TASKMODAL);
	PostQuitMessage(0);
}
else
{
	// load the various wordlists, etc.  These lists are used very often,
	// and are correctly stored in local wordlists, but I think
	// I intended that the data eventually be stored in MDB tables, and loaded from there.
	FACT::Contractions.Load("expand.dat",TRUE);   	// TRUE = auto-write any changes
	FACT::ContextReverser.Load("reverse.dat",FALSE);
	FACT::iswords.Load("iswords.dat",FALSE);
	FACT::InfiniVerbs.Load("infinivb.dat",FALSE);
	FACT::Pronouns.Load("pronouns.dat",FALSE);
	FACT::YNQStarters.Load("ynqstart.dat",FALSE);

	// RECORD WHETHER MSACCESS IS ALREADY RUNNING.
	// general method will find first MSAccess instance, any database.
	MSAccessAtStartup = FindWindow("OMain", NULL);
	// this is probably too specific: database window must be focus window, and maximized
	// MSAccessAtStartup = FindWindow("OMain", "Microsoft Access - [Database: WTALK]");

	// OPEN WTALK.MDB RIGHT AWAY, SO IT'S PART OF THE STARTUP PROCESS.
	// (this also allows using only WTALK for the database name throughout program)
	SDDEHandler* handler = dynamic_cast<SDDEHandler*>(GetMainWindow()->GetClientWindow());
	if(handler)
	{
		SDDEConv* chan1 = handler->DDEInitiate("MSACCESS",DATADir + "\\WTALK.MDB");
		if(chan1)
		{
			chan1->DDEExecute("[Maximize]");
			chan1->DDEExecute("[SetWarnings 0]");  	// required to prevent msgs, even for DDE

// 		// clear all old CPI question answers
	// 		// #error recheck: not sure this ever worked
	// 		string t("[RUNSQL \"UPDATE DISTINCTROW CPIQuestions "
	// 				 "SET CPIQuestions.Answer = \"\"U\"\", CPIQuestions.Marked = No;\"]");
	// 		chan1->DDEExecute(t);

			handler->DDETerminate(chan1);
		}
		else
		{
			::MessageBox(GetFocus(),"Application cannot start.  File WTALK.MDB not found.",
							GetName(), MB_ICONSTOP|MB_TASKMODAL);
			PostQuitMessage(0);
		}
	}
}
}                       	//InitInstance
//----------------------------------------------------------------------------
void TMyApp::InitMainWindow()
{
EnableCtl3d(TRUE);				// these are better.  makes all controls automatically 3d.

const char* p = ((DDESide == SERVER) && BaseServiceName.length()) ? BaseServiceName.c_str() : 0;
STalkerWindow* client = new STalkerWindow(0, 0, InstId, p);

TDecoratedFrame* frame = new TDecoratedFrame(0, Name, client, TRUE);
frame->AssignMenu(TResId(MAINMENU));
frame->SetMenuDescr(TMenuDescr(TResId(MAINMENU),1,1,0,2,0,1));
frame->Attr.AccelTable = TResId(MAINMENU);
frame->EnableKBHandler();

TStatusBar* sb = new TStatusBar(frame, TGadget::Recessed);
frame->Insert(*sb, TDecoratedFrame::Bottom);

nCmdShow = SW_SHOWMAXIMIZED;		// SW_SHOW;
SetMainWindow(frame);
}						//InitMainWindow
//----------------------------------------------------------------------------
int TMyApp::TermInstance(int status)
{
// a system crash can occur if this app launches another app, and then this one closes before the other one.
// (this hasn't been a problem in Win16 until I started launching WTalk FROM Winword.)
// so we must try to decide IF this app launched the other.
// (it actually occurs inside DDEInitiate).
// if MSAccess wasn't running at startup, but is now, OR
// if the MSAccess instance that WAS running at startup is different from the one running now,
// assume that we launched its current instance.
// Not foolproof, but it's the best guess we can make.
// there can be multiple MSAccess instances running, and the method I use here is crude.
HWND MSAccessAtClose = FindWindow("OMain", NULL);
if(MSAccessAtClose)
	if(!MSAccessAtStartup || (MSAccessAtStartup != MSAccessAtClose))
	{
		SDDEHandler* handler = dynamic_cast<SDDEHandler*>(GetMainWindow()->GetClientWindow());
		if(handler)
		{
			SDDEConv* chan1 = handler->DDEInitiate("MSACCESS","WTALK");
			if(chan1)
			{
				chan1->DDEExecute("[Quit 0]");	// 0 = prompt for changed objects
				handler->DDETerminate(chan1);
			}
		}
	}

return SDDEApplication::TermInstance(status);	// call this last
}                      		//TermInstance
//----------------------------------------------------------------------------
// 						end class TMyApp
//////////////////////////////////////////////////////////////////////////////
// OwlMain
int OwlMain(int /* argc */, char* /* argv */ [])
{
randomize();
string::set_case_sensitive(0);
TTime::PrintDate(TRUE);
TDate::SetPrintOption(TDate::Numbers);

// Run() calls: InitApplication(), InitInstance() { (which calls: InitMainWindow() },
// then displays main window.
TMyApp* myapp = new TMyApp(AppName, SERVER, "WTALK");
int retval = myapp->Run();
delete myapp;

return(retval);
}

 

 

Valid HTML 4.01 Transitional Valid CSS
View content labeling at ICRA.
Copyright ©2008 Steven Whitney. Last modified 10/06/2008.