Hangman With Nothing

October 2019 | Ludum Dare 45: Start With Nothing



Project Information

  • Language: C++
  • Tools: Visual Studio, OpenGL 3
  • itch.io: link
  • Download: link

Gameplay

In this game, the gameplay is quite similar to traditional Hangman. The only difference is that when you want to try to solve the puzzles, you will use the text bricks you hit down from the hints. If the bricks run out, the game is over.


Controls

  • Arrow Keys - Move
  • Space - Break the text
  • A~Z - Try out the characters

Game.cpp

The Game code is used for the core loop of the game. Nearly all game related logic are written here.

#include "Engine/Math/MathUtils.hpp"
#include "Engine/Core/ErrorWarningAssert.hpp"
#include "Engine/Input/InputSystem.hpp"
#include "Engine/Core/Time.hpp"

#include "Game/Game.hpp"
#include "Game/GameCommon.hpp"
#include "Game/App.hpp"
#include "Game/TextHelper.hpp"
#include "Engine/Math/AABB2.hpp"
#include "Game/Dot.hpp"
#include "Game/Cursor.hpp"

TextHelper* m_textHelper = nullptr;


const char* puzzleHints[] = {
	"ARROWS TO MOVE THE CURSOR\nSPACE TO BREAK TEXTS\nF1 TO RESTART",
	"BINARY",
	"MAKE AMERICA GREAT AGAIN",
	"LAW OF UNIVERSAL GRAVITATION",
	"THREAD",
	"PEN APPLE",
	"Color 0",
	"MINIONS",
	"SHAPE OF CURSOR",
	"FIRST ELECTRONIC\nGENERAL-PURPOSE COMPUTER",
	"DRAGON QUEST",
	"SHIGERU MIYAMOTO",
	"BLADE RUNNER",
	"STEINS;GATE",
	"GAMES WITH TEXT BASED STORIES",
	"YANKEES",
	"ARTIFICIAL INTELLIGENCE",
	"HIGHEST MOUNTAIN",
	"FIRST MARVEL SUPERHERO",
	"START WITH",
};

const char* puzzleWords[] = {
	"TUTORIAL",
	"TWO",
	"TRUMP",
	"NEWTON",
	"NEEDLE",
	"PINEAPPLE",
	"BLACK",
	"BANANA",
	"SQUARE",
	"ENIAC",
	"SLIME",
	"NINTENDO",
	"CYBERPUNK",
	"TIME MACHINE",
	"VISUAL NOVEL",
	"NEW YORK",
	"NEURAL NETWORK",
	"EVEREST",
	"HUMAN TORCH",
	"NOTHING",
};

const int levelNum = 20;


Game::Game()
{
	m_textHelper = new TextHelper();
	m_textHelper->StartUp();
	m_textHelper->m_game = this;
	m_startGame = false;
	m_restartGame = false;
	m_worldCamera.SetOrthoView(Vec2(0, 0), Vec2(WORLD_SIZE_X, WORLD_SIZE_Y));
	m_uiCamera.SetOrthoView(Vec2(0, 0), Vec2(WORLD_SIZE_X, WORLD_SIZE_Y));

	//m_currentLevel = 19;
	m_cursor = new Cursor(this, Vec2(WORLD_CENTER_X, WORLD_CENTER_Y - 24));
	GenerateNewPuzzle();
}

Game::~Game()
{

}

const void Game::BeginFrame()
{

}

const void Game::Update( float deltaSeconds )
{
	static float xShift = 0;
	static float yShift = 0;
	if (m_screenShakeIntensity > 0)
	{
		xShift = m_rng->GetRandomFloatInRange(0, m_screenShakeIntensity);
		yShift = m_rng->GetRandomFloatInRange(0, m_screenShakeIntensity);
		m_screenShakeIntensity -= m_screenShakeDecreaseSpeed * deltaSeconds;
	}
	m_worldCamera.SetOrthoView(Vec2(0, 0), Vec2(WORLD_SIZE_X, WORLD_SIZE_Y));
	m_worldCamera.Translate(Vec2(xShift, yShift));

	UpdateFromKeyboard(deltaSeconds);
	UpdateFromJoystick();

	//TextAnimationTest();
	UpdateGameState();
	//CreateStoryBoard();
	//CheckNextLevel();
	UpdateDots(deltaSeconds);

	m_gameLastTime = m_gameTime;
	m_gameTime += deltaSeconds;

}

const void Game::UpdateDots(float deltaSeconds) const
{
	for (int i = 0; i < m_deadDots.size(); i++)
	{
		m_deadDots[i]->Update(deltaSeconds);
	}
	for (int i = 0; i < m_dots.size(); i++)
	{
		m_dots[i]->Update(deltaSeconds);
	}
}

const void Game::Render() const
{
	Rgba8 black = Rgba8(0, 0, 0, 255);
	g_theRenderer->ClearScreen(black);

	g_theRenderer->BeginCamera(m_worldCamera);
	RenderWorld();
	g_theRenderer->EndCamera(m_worldCamera);

	g_theRenderer->BeginCamera(m_uiCamera);
	//RenderUI();
	g_theRenderer->EndCamera(m_uiCamera);

	if (g_theApp->m_isDebugMode)
	{
		DebugRender();
	}

}

const void Game::CleanUp()
{
	for (int i = 0; i < m_dots.size(); i++)
	{
		delete (m_dots[i]);
	}
	m_dots.clear();

	for (int i = 0; i < m_deadDots.size(); i++)
	{
		delete (m_deadDots[i]);
	}
	m_deadDots.clear();
	m_numDeadDots = 0;

	for (int i = 0; i < 512; i++)
		m_wrongText[i] = '\0';
	m_wrongTextNum = 0;
}

const void Game::GameOverCleanUp()
{
	for (int i = 0; i < m_dots.size(); i++)
	{
		if (!m_dots[i]->HasPhysics()) {
			m_dots[i]->m_isAutoUpdate = true;  m_dots[i]->Die();
		}
	}

	for (int i = 0; i < 512; i++)
		m_wrongText[i] = '\0';
	m_wrongTextNum = 0;
}

const void Game::RenderWorld() const
{
	RenderDots();
	RenderUI();

	RenderCursor();
	g_theRenderer->DrawVertexArray(m_puzzleVertexes);
}

const void Game::RenderDots() const
{
	static std::vector renderVertexes;
	renderVertexes.clear();

	for (int i = 0; i < m_dots.size(); i++)
	{
		m_dots[i]->AddForRender(renderVertexes);
	}
	for (int i = 0; i < m_deadDots.size(); i++)
	{
		m_deadDots[i]->AddForRender(renderVertexes);
	}
	g_theRenderer->DrawVertexArray(renderVertexes);
}

const void Game::RenderUI() const
{
	if (m_gameState != GS_GAMEOVER)
		m_textHelper->DrawString((const char*)m_wrongText, 1.f, Vec2(WORLD_CENTER_X, 50), Vec2(.5f, .5f), Rgba8(255, 0, 0, 255), 4);

}

const void Game::DebugRender() const
{
	//if (m_gameState != GS_PLAYING) return;
	char buff[128];

	sprintf_s(buff, 128, "Dot Num %d", m_dots.size() + m_deadDots.size());
	m_textHelper->DrawString((const char*)(buff), .5f, Vec2(20, 150), Vec2(0, 1), Rgba8(0, 255, 0, 255));
}

const void Game::EndFrame()
{
	CleanGarbage();
}

const void Game::UpdateGameState()
{
	if (CheckWordFinished() && m_gameState == GS_GUESSING) {
		m_gameTime = 0;
		m_gameLastTime = 0;
		m_gameState = GS_FINISHEDLEVEL;
	}
	if (m_gameState == GS_FINISHEDLEVEL && m_gameTime > 1) {
		if (m_currentLevel < levelNum - 1)
		{
			m_currentLevel++;
			GenerateNewPuzzle();
		}
		else
		{
			CleanUp();
			m_gameState = GS_GAMEOVER;

			m_puzzleVertexes.clear();
			RenderGameOverContext();
		}
	}
}

const void Game::RenderGameOverContext()
{
	char buff[128];

	m_textHelper->CreateDotsFromString(m_dots, "THANKS FOR YOUR PLAYING!", 1.f, Vec2(WORLD_CENTER_X, 150), Vec2(.5f, .5f), GetRandomColor(), false);
	m_textHelper->CreateDotsFromString(m_dots, "Result", .8f, Vec2(WORLD_CENTER_X - 60, 80), Vec2(.5f, .5f), GetRandomColor(), false);
	sprintf_s(buff, 128, "Restart %d times\nFail trials %d times", m_gameRestartNum, m_gameWrongCharNum);
	m_textHelper->CreateDotsFromString(m_dots, (const char*)buff, .7f, Vec2(WORLD_CENTER_X, 55), Vec2(.5f, .5f), GetRandomColor(), false);
	m_textHelper->CreateDotsFromString(m_dots, "Created by Yao Shen          2019", .5f, Vec2(WORLD_CENTER_X, 10), Vec2(.5f, .5f), GetRandomColor(), false);
}

const void Game::CleanGarbage()
{
	std::vector::iterator it = m_dots.begin();

	while (it != m_dots.end()) {

		if ((*it)->IsGarbage()) {

			it = m_dots.erase(it);
		}
		else ++it;
	}

	it = m_deadDots.begin();

	while (it != m_deadDots.end()) {

		if ((*it)->IsGarbage()) {

			it = m_deadDots.erase(it);
		}
		else ++it;
	}
}

const void Game::UpdateFromKeyboard(float deltaSeconds)
{
	if (g_theInput->WasKeyJustPressed(KEY_LEFTARROW))
	{
		if (m_cursor->GetPosition().x - m_cursor->m_cursorSize / 2 > 0)
			m_cursor->Translate(Vec2(-m_cursor->m_cursorSize / 2, 0));
	}

	if (g_theInput->WasKeyJustPressed(KEY_RIGHTARROW))
	{
		if (m_cursor->GetPosition().x + m_cursor->m_cursorSize / 2 < WORLD_SIZE_X)
			m_cursor->Translate(Vec2(m_cursor->m_cursorSize / 2, 0));
	}

	if (g_theInput->WasKeyJustPressed(KEY_UPARROW))
	{
		if (m_cursor->GetPosition().y + m_cursor->m_cursorSize / 2 < WORLD_SIZE_Y)
			m_cursor->Translate(Vec2(0, m_cursor->m_cursorSize / 2));
	}

	if (g_theInput->WasKeyJustPressed(KEY_DOWNARROW))
	{
		if (m_cursor->GetPosition().y - m_cursor->m_cursorSize / 2 > 0)
			m_cursor->Translate(Vec2(0, -m_cursor->m_cursorSize / 2));
	}

	if (!g_theInput->GetKeyState(KEY_LEFTARROW).IsPressed()) { m_leftDownTime = 0; m_leftLongPress = false; }
	if (!g_theInput->GetKeyState(KEY_RIGHTARROW).IsPressed()){ m_rightDownTime = 0; m_rightLongPress = false;}
	if (!g_theInput->GetKeyState(KEY_UPARROW).IsPressed()) { m_upDownTime = 0; m_upLongPress = false; }
	if (!g_theInput->GetKeyState(KEY_DOWNARROW).IsPressed()) { m_downDownTime = 0; m_downLongPress = false; }

	if (g_theInput->GetKeyState(KEY_LEFTARROW).IsPressed()) {
		m_leftDownTime += deltaSeconds;
		if (m_leftDownTime > 0.5f) m_leftLongPress = true;
		if (m_leftDownTime > 0.1f && m_leftLongPress) {
			if (m_cursor->GetPosition().x - m_cursor->m_cursorSize / 2 > 0)
				m_cursor->Translate(Vec2(-m_cursor->m_cursorSize / 2, 0));
			m_leftDownTime = 0;
		}
	}

	if (g_theInput->GetKeyState(KEY_RIGHTARROW).IsPressed()) {
		m_rightDownTime += deltaSeconds;
		if (m_rightDownTime > 0.5f) m_rightLongPress = true;
		if (m_rightDownTime > 0.1f && m_rightLongPress) {
			if (m_cursor->GetPosition().x + m_cursor->m_cursorSize / 2 < WORLD_SIZE_X)
				m_cursor->Translate(Vec2(m_cursor->m_cursorSize / 2, 0));
			m_rightDownTime = 0;
		}
	}

	if (g_theInput->GetKeyState(KEY_UPARROW).IsPressed()) {
		m_upDownTime += deltaSeconds;
		if (m_upDownTime > 0.5f) m_upLongPress = true;
		if (m_upDownTime > 0.1f && m_upLongPress) {
			if (m_cursor->GetPosition().y + m_cursor->m_cursorSize / 2 < WORLD_SIZE_Y)
				m_cursor->Translate(Vec2(0, m_cursor->m_cursorSize / 2));
			m_upDownTime = 0;
		}
	}

	if (g_theInput->GetKeyState(KEY_DOWNARROW).IsPressed()) {
		m_downDownTime += deltaSeconds;
		if (m_downDownTime > 0.5f) m_downLongPress = true;
		if (m_downDownTime > 0.1f && m_downLongPress) {
			if (m_cursor->GetPosition().y - m_cursor->m_cursorSize / 2 > 0)
				m_cursor->Translate(Vec2(0, -m_cursor->m_cursorSize / 2));
			m_downDownTime = 0;
		}
	}

	if (g_theInput->WasKeyJustPressed(KEY_SPACEBAR))
	{
	 	BreakBricks();
		if (m_gameState == GS_GAMEOVER) {
			GameOverCleanUp();
			RenderGameOverContext();
		}
	}

	//for (unsigned char ch = '0'; ch <= '9'; ch++) {
	//	if (g_theInput->WasKeyJustPressed(ch) && m_deadDots.size() > m_textHelper->GetPointNum(ch)) {
	//		m_playerText[m_playerTextNum] = ch;
	//		m_playerTextNum++;
	//		RemoveDeadDotTile(m_textHelper->GetPointNum(ch));
	//	}
	//}

	for (unsigned char ch = 'A'; ch <= 'Z'; ch++) {
		if (g_theInput->WasKeyJustPressed(ch)) {
			ScreenShake(.4f, .2f);
		}
		if (g_theInput->WasKeyJustPressed(ch) && m_deadDots.size() > m_textHelper->GetPointNum(ch)) {
			if (CheckHitAnswer(ch) != -1) {
				m_textHelper->CreateDotsFromChar(m_dots, ch, 1.f, charPosition[CheckHitAnswer(ch)], Rgba8(255, 255, 128, 255), true);
				isCharHit[CheckHitAnswer(ch)] = true;
			} else {
				if (m_wrongTextNum == 9 * 4 - 1) {
					return;
				}
				if (((m_wrongTextNum + 1) % 9) == 0) {
					m_wrongText[m_wrongTextNum] = '\n';
					m_wrongTextNum++;
				}

				m_wrongText[m_wrongTextNum] = ch;
				m_wrongTextNum++;

				m_gameWrongCharNum++;

			}
			RemoveDeadDotTile(m_textHelper->GetPointNum(ch));
		}
	}

	if (g_theInput->GetKeyState(KEY_F1).WasJustPressed())
	{
		if (m_gameState == GS_GUESSING) {
			RestartCurrentLevel();
			m_gameRestartNum++;
		}
		if (m_gameState == GS_GAMEOVER) {
			CleanUp();

			RenderGameOverContext();
		}

	}

	//if (g_theInput->WasKeyJustPressed(KEY_BACKSPACE)) {
	//	if (m_playerTextNum > 0) {
	//		unsigned char ch = m_playerText[m_playerTextNum - 1];
	//		m_playerText[m_playerTextNum-1] = '\0';
	//		m_playerTextNum--;
	//		for (int i = 0; i < m_textHelper->GetPointNum(ch); i++) {
	//			AddDeadDotTile();
	//		}
	//	}
	//}

	if (g_theInput->WasKeyJustPressed(KEY_ESC)) {
		g_theApp->m_isQuitting = true;
	}

}

const void Game::UpdateFromJoystick()
{
	// ONLY TAKE COMMANDS FROM THE FIRST CONTROLLER!!
	if (!g_theInput->GetXboxController(0).IsConnected())
		return;

}

const void Game::BreakBricks()
{
	int m_breaknum = 0;
	for (int i = 0; i < m_dots.size(); i++)
	{
		if (m_dots[i]->HasPhysics() || m_dots[i]->m_isDeadDot) continue;
		AABB2 dotBox = m_dots[i]->GetAABB2();
		AABB2 cursorArea = m_cursor->GetAABB2();
		if (DoAABBsOverlap2D(dotBox, cursorArea)) {
			float randomAngle = m_rng->GetRandomFloatInRange(45, 135);
			Vec2 randomDir = Vec2::MakeFromPolarDegrees(randomAngle, m_rng->GetRandomFloatInRange(20, 50));
			m_dots[i]->SetPhysics(randomDir);
			m_breaknum++;
		}
	}

	if (m_breaknum != 0)
	{
		ScreenShake(2.f, .5f);

		m_breakBricks = true;
	}
}

const void Game::AddDeadDotTile(const Rgba8& color)
{
	float pos_x = m_numDeadDots % 256;
	float pos_y = m_numDeadDots / 256;
	Dot* newDOT = new Dot(this, Vec2(pos_x, pos_y), 1.f, color);
	newDOT->m_isDeadDot = true;
	m_deadDots.push_back(newDOT);
	m_numDeadDots++;
}


const char* Game::GenerateNewWord()
{
	return puzzleWords[m_currentLevel];
}

const void Game::GenerateNewPuzzle()
{
	CleanUp();

	m_gameState = GS_GUESSING;
	m_textHelper->CreateDotsFromString(m_dots, puzzleHints[m_currentLevel], 1.f, Vec2(WORLD_CENTER_X, WORLD_SIZE_Y - 40), Vec2(.5f, .5f), Rgba8(255, 255, 255, 255), false);


	char buff[128];
	sprintf_s(buff, 128, "%d", m_currentLevel + 1);
	m_textHelper->CreateDotsFromString(m_dots, buff, 1.f, Vec2(WORLD_SIZE_X - 8, WORLD_SIZE_Y - 10), Vec2(1.f, .5f), Rgba8(255, 255, 255, 255), false);

	m_puzzleLength = 0;
	const char* word = GenerateNewWord();
	m_currentWord = word;

	for (int i = 0; word[i] != '\0'; i++)
		m_puzzleLength++;

	float phyLength = m_puzzleLength * 8 + (m_puzzleLength - 1) * 4;
	float startX = WORLD_CENTER_X - phyLength / 2;
	float pY = WORLD_CENTER_Y + 10;

	std::vector vertexes;
	charPosition.clear();
	isCharHit.clear();
	for (int i = 0; i < m_puzzleLength; i++) {
		if (word[i] != ' ')
			AppendVertsForLine2D(vertexes, Vec2(startX, pY), Vec2(startX + 8, pY), Rgba8(255, 255, 255, 255), .5f);
		charPosition.push_back(Vec2(startX - 2.5f, pY));
		isCharHit.push_back(false);
		startX += 12;
	}

	m_puzzleVertexes = vertexes;
}

const int Game::CheckHitAnswer(unsigned char ch)
{
	for (int i = 0; i < m_puzzleLength; i++)
		if (ch == m_currentWord[i] && !isCharHit[i]) return i;
	return -1;
}

Rgba8 Game::GetRandomColor()
{
	int r = m_rng->GetRandomIntInRange(0, 255);
	int g = m_rng->GetRandomIntInRange(0, 255);
	int b = m_rng->GetRandomIntInRange(0, 255);
	while (r + g + b < 400)
	{
		r = m_rng->GetRandomIntInRange(0, 255);
		g = m_rng->GetRandomIntInRange(0, 255);
		b = m_rng->GetRandomIntInRange(0, 255);
	}
	return Rgba8(r, g, b, 255);
}

const void Game::RemoveDeadDotTile(int num)
{
	for (int i = 0; i < num; i++)
	{
		Dot* dot = m_deadDots[m_deadDots.size() - 1];
		dot->m_isAutoUpdate = true;
		dot->Die();
		m_deadDots.pop_back();
	}
	m_numDeadDots -= num;
}

const void Game::CleanAliveDots()
{
	for (int i = 0; i < m_dots.size(); i++)
	{
		if (!m_dots[i]->IsGarbage()) m_dots[i]->m_isAutoUpdate = true;
		if (!m_dots[i]->HasPhysics())
			m_dots[i]->Die();
	}
}

const void Game::ScreenShake(float intensity, float shakeTime)
{
	m_screenShakeIntensity = intensity;
	m_screenShakeDecreaseSpeed = intensity / shakeTime;
}

const void Game::CreateTextAnimation(const char* text, float startTime, float interval, const Vec2& pivot, GameState atGameState)
{
	if (m_gameState != atGameState) return;
	if (m_gameTime < startTime) return;

	char buff[512];
	int i = 0;
	while (i == 0 || text[i-1] != '\0')
	{
		if (m_gameLastTime < (startTime + interval * i) && m_gameTime >= (startTime + interval * i)) {
			strncpy_s(buff, text, i);
			CleanAliveDots();
			m_textHelper->CreateDotsFromString(m_dots, buff, 1.f, Vec2(WORLD_CENTER_X, WORLD_CENTER_Y), pivot, Rgba8(127, 127, 127, 255), false);
			m_breakBricks = false;
		}
		i++;
	}

}

const void Game::CreateStoryBoard()
{
	//CreateTextAnimation("Hello", 0.f, .15f, Vec2(.5f, .5f), GS_WELCOME);
	//CreateTextAnimation("I'm Artificial Intelligence\n Stella, I've been waiting for you so long.", 2.f, .15f, Vec2(.5f, .5f), GS_WELCOME);
	//CreateTextAnimation("Ok, this is the game for\nLudam Dare 45. I agree I\nmade a mistake by making\ntexts breakable.", .5f, .1f, Vec2(.5f, .5f), GS_S1);
	//CreateTextAnimation("Please, stop breaking my\ntexts. I know I'm a bad\nwritter though...", .5f, .1f, Vec2(.5f, .5f), GS_S2);
	//CreateTextAnimation("Nothing more, enjoy breaking the texts here.", .5f, .1f, Vec2(.5f, .5f), GS_S3);

	//char buff[512];
	//for (int i = 0; i < 256; i++) {
	//	int randType = m_rng->GetRandomIntInRange(0, 2);
	//	if (randType == 0)
	//		buff[i] = '0' + m_rng->GetRandomIntInRange(0, 9);
	//	else if (randType == 1)
	//		buff[i] = 'A' + m_rng->GetRandomIntInRange(0, 25);
	//	else
	//		buff[i] = 'a' + m_rng->GetRandomIntInRange(0, 25);

	//	if ((i+1) % 32 == 0) buff[i] = '\n';
	//}
	//buff[128] = '/0';

	//CreateTextAnimation((const char*)buff, .5f, .1f, Vec2(.5f, .5f), GS_S4);
}

bool Game::CheckWordFinished()
{
	for (int i = 0; i < m_puzzleLength; i++) {
		if (!isCharHit[i] && m_currentWord[i] != ' ') return false;
	}
	return true;
}

void Game::RenderCursor() const
{
	m_cursor->Render();
}

void Game::RestartCurrentLevel()
{
	GenerateNewPuzzle();
}


            

Post Mortem


What Went Well

  • Successfully developed a simple fun game in 2 days.

What Went Wrong

  • Didn't quite get a game idea for the contest until the last 24 hours.

What I learned

  • It's fun to develop a game with fresh new ideas.