diff --git a/demos/TicTacToe/.gitignore b/demos/TicTacToe/.gitignore new file mode 100644 index 00000000..e60b43e4 --- /dev/null +++ b/demos/TicTacToe/.gitignore @@ -0,0 +1,2 @@ +tic_tac_toe_demo/ +google-services.json \ No newline at end of file diff --git a/demos/TicTacToe/game_resources/CMakeLists.txt b/demos/TicTacToe/game_resources/CMakeLists.txt new file mode 100644 index 00000000..e103236f --- /dev/null +++ b/demos/TicTacToe/game_resources/CMakeLists.txt @@ -0,0 +1,208 @@ +#/**************************************************************************** + # Copyright 2020 Google Inc. All rights reserved. + # + # Licensed under the Apache License, Version 2.0 (the "License"); + # you may not use this file except in compliance with the License. + # You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License is distributed on an "AS IS" BASIS, + # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + # See the License for the specific language governing permissions and + # limitations under the License. +#/**************************************************************************** + +#/**************************************************************************** + # Copyright (c) 2013-2014 cocos2d-x.org + # Copyright (c) 2015-2017 Chukong Technologies Inc. + # + # http://www.cocos2d-x.org + # + # Permission is hereby granted, free of charge, to any person obtaining a copy + # of this software and associated documentation files (the "Software"), to deal + # in the Software without restriction, including without limitation the rights + # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + # copies of the Software, and to permit persons to whom the Software is + # furnished to do so, subject to the following conditions: + + # The above copyright notice and this permission notice shall be included in + # all copies or substantial portions of the Software. + + # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + # THE SOFTWARE. +# ****************************************************************************/ + +cmake_minimum_required(VERSION 3.6) + +set(APP_NAME tic_tac_toe_demo) + +project(${APP_NAME}) + +# User settings for Firebase samples. +# Path to Firebase SDK. +# Try to read the path to the Firebase C++ SDK from an environment variable. +if (NOT "$ENV{FIREBASE_CPP_SDK_DIR}" STREQUAL "") + set(DEFAULT_FIREBASE_CPP_SDK_DIR "$ENV{FIREBASE_CPP_SDK_DIR}") +else() + set(DEFAULT_FIREBASE_CPP_SDK_DIR "firebase_cpp_sdk") +endif() +if ("${FIREBASE_CPP_SDK_DIR}" STREQUAL "") + set(FIREBASE_CPP_SDK_DIR ${DEFAULT_FIREBASE_CPP_SDK_DIR}) +endif() +if(NOT EXISTS ${FIREBASE_CPP_SDK_DIR}) + message(FATAL_ERROR "The Firebase C++ SDK directory does not exist: ${FIREBASE_CPP_SDK_DIR}. See the readme.md for more information") +endif() + +# Build a desktop application. +# Windows runtime mode, either MD or MT depending on whether you are using +# /MD or /MT. For more information see: +# https://msdn.microsoft.com/en-us/library/2kzt1wy3.aspx +set(MSVC_RUNTIME_MODE MD) + +if(APPLE) + set(ADDITIONAL_LIBS gssapi_krb5 pthread "-framework CoreFoundation" "-framework Foundation" "-framework GSS" "-framework Security") +elseif(MSVC) + set(ADDITIONAL_LIBS advapi32 ws2_32 crypt32 iphlpapi psapi userenv) +else() + set(ADDITIONAL_LIBS pthread) +endif() + +# If a config file is present, copy it into the binary location so that it's +# possible to create the default Firebase app. +set(FOUND_JSON_FILE FALSE) +foreach(config "google-services-desktop.json" "google-services.json") + if (EXISTS ${config}) + add_custom_command( + TARGET ${APP_NAME} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy + ${config} $) + set(FOUND_JSON_FILE TRUE) + break() + endif() +endforeach() +if(NOT FOUND_JSON_FILE) + message(WARNING "Failed to find either google-services-desktop.json or google-services.json. See the readme.md for more information.") +endif() +#Target name change to cocos +# Add the Firebase libraries to the target using the function from the SDK. +add_subdirectory(${FIREBASE_CPP_SDK_DIR} bin/ EXCLUDE_FROM_ALL) + +# # Note that firebase_app needs to be last in the list. +set(firebase_libs firebase_database firebase_auth firebase_app) + +if(XCODE) + if(NOT DEFINED CMAKE_XCODE_ATTRIBUTE_IPHONEOS_DEPLOYMENT_TARGET) + SET (CMAKE_XCODE_ATTRIBUTE_IPHONEOS_DEPLOYMENT_TARGET 8.0) + endif() +endif() + +if(NOT DEFINED BUILD_ENGINE_DONE) # to test install_test into root project + set(COCOS2DX_ROOT_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cocos2d) + set(CMAKE_MODULE_PATH ${COCOS2DX_ROOT_PATH}/cmake/Modules/) + include(CocosBuildSet) + add_subdirectory(${COCOS2DX_ROOT_PATH}/cocos ${ENGINE_BINARY_PATH}/cocos/core) +endif() + +# record sources, headers, resources... +set(GAME_HEADER) +set(GAME_SOURCE) +set(GAME_RES_FOLDER "${CMAKE_CURRENT_SOURCE_DIR}/Resources") + + list(APPEND GAME_SOURCE + Classes/app_delegate.cc + Classes/tic_tac_toe_layer.cc + Classes/tic_tac_toe_scene.cc + Classes/main_menu_scene.cc + Classes/util.cc + ) + +list(APPEND GAME_HEADER + Classes/app_delegate.h + Classes/tic_tac_toe_layer.h + Classes/tic_tac_toe_scene.h + Classes/main_menu_scene.h + Classes/util.h + ) + +if(APPLE OR WINDOWS) + cocos_mark_multi_resources(common_res_files RES_TO "Resources" FOLDERS ${GAME_RES_FOLDER}) +endif() + +# add cross-platforms source files and header files +list(APPEND GAME_SOURCE Classes/app_delegate.cc ) +list(APPEND GAME_HEADER Classes/app_delegate.h ) + +if(ANDROID) + # change APP_NAME to the share library name for Android, it's value depend on AndroidManifest.xml + set(APP_NAME DemoApp) + list(APPEND GAME_SOURCE proj.android/app/jni/hellocpp/main.cpp) +elseif(LINUX) + list(APPEND GAME_SOURCE proj.linux/main.cpp) +elseif(WINDOWS) + list(APPEND GAME_HEADER proj.win32/main.h proj.win32/resource.h) + list(APPEND GAME_SOURCE proj.win32/main.cpp proj.win32/game.rc ${common_res_files}) +elseif(APPLE) + if(IOS) + list(APPEND GAME_HEADER proj.ios_mac/ios/AppController.h + proj.ios_mac/ios/RootViewController.h) + set(APP_UI_RES proj.ios_mac/ios/LaunchScreen.storyboard + proj.ios_mac/ios/LaunchScreenBackground.png + proj.ios_mac/ios/Images.xcassets) + list(APPEND GAME_SOURCE proj.ios_mac/ios/main.m proj.ios_mac/ios/AppController.mm + proj.ios_mac/ios/RootViewController.mm proj.ios_mac/ios/Prefix.pch ${APP_UI_RES}) + elseif(MACOSX) + set(APP_UI_RES proj.ios_mac/mac/Icon.icns proj.ios_mac/mac/Info.plist) + list(APPEND GAME_SOURCE proj.ios_mac/mac/main.cpp + proj.ios_mac/mac/Prefix.pch ${APP_UI_RES}) + endif() + list(APPEND GAME_SOURCE ${common_res_files}) +endif() + +# mark app complie info and libs info +set(all_code_files ${GAME_HEADER} ${GAME_SOURCE}) +if(NOT ANDROID) + add_executable(${APP_NAME} ${all_code_files}) +else() + add_library(${APP_NAME} SHARED ${all_code_files}) + add_subdirectory(${COCOS2DX_ROOT_PATH}/cocos/platform/android + ${ENGINE_BINARY_PATH}/cocos/platform) + target_link_libraries(${APP_NAME} -Wl,--whole-archive cpp_android_spec + -Wl,--no-whole-archive) +endif() + +target_link_libraries(${APP_NAME} cocos2d "${firebase_libs}" ${ADDITIONAL_LIBS}) +target_include_directories(${APP_NAME} PRIVATE Classes + PRIVATE ${COCOS2DX_ROOT_PATH}/cocos/audio/include/) + +# mark app resources +setup_cocos_app_config(${APP_NAME}) +if(APPLE) + set_target_properties(${APP_NAME} PROPERTIES RESOURCE "${APP_UI_RES}") + if(MACOSX) + set_xcode_property(${APP_NAME} INFOPLIST_FILE + "${CMAKE_CURRENT_SOURCE_DIR}/proj.ios_mac/mac/Info.plist") + elseif(IOS) + set_xcode_property(${APP_NAME} INFOPLIST_FILE + "${CMAKE_CURRENT_SOURCE_DIR}/proj.ios_mac/ios/Info.plist") + set_xcode_property(${APP_NAME} ASSETCATALOG_COMPILER_APPICON_NAME "AppIcon") + endif() + +# For code-signing, set the DEVELOPMENT_TEAM: +#set_xcode_property(${APP_NAME} DEVELOPMENT_TEAM "GRLXXXX2K9") +elseif(WINDOWS) + cocos_copy_target_dll(${APP_NAME}) +endif() + +if(LINUX OR WINDOWS) + cocos_get_resource_path(APP_RES_DIR ${APP_NAME}) + cocos_copy_target_res(${APP_NAME} LINK_TO ${APP_RES_DIR} + FOLDERS ${GAME_RES_FOLDER}) +endif() + diff --git a/demos/TicTacToe/game_resources/Classes/app_delegate.cc b/demos/TicTacToe/game_resources/Classes/app_delegate.cc new file mode 100644 index 00000000..bf24d013 --- /dev/null +++ b/demos/TicTacToe/game_resources/Classes/app_delegate.cc @@ -0,0 +1,49 @@ +// Copyright 2020 Google Inc. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "app_delegate.h" + +#include "cocos2d.h" +#include "main_menu_scene.h" + +using cocos2d::Director; +using cocos2d::GLViewImpl; + +// Set based on the image width. +const float kFrameWidth = 600; + +// Set based on the image height plus 40 for windows bar. +const float kFrameHeight = 640; + +AppDelegate::AppDelegate() {} + +AppDelegate::~AppDelegate() {} +bool AppDelegate::applicationDidFinishLaunching() { + auto director = Director::getInstance(); + auto glview = director->getOpenGLView(); + if (glview == NULL) { + glview = GLViewImpl::create("Firebase Tic-Tac-Toe"); + glview->setFrameSize(kFrameWidth, kFrameHeight); + director->setOpenGLView(glview); + } + + auto scene = MainMenuScene::createScene(); + director->runWithScene(scene); + + return true; +} + +void AppDelegate::applicationDidEnterBackground() {} + +void AppDelegate::applicationWillEnterForeground() {} \ No newline at end of file diff --git a/demos/TicTacToe/game_resources/Classes/app_delegate.h b/demos/TicTacToe/game_resources/Classes/app_delegate.h new file mode 100644 index 00000000..86c15e48 --- /dev/null +++ b/demos/TicTacToe/game_resources/Classes/app_delegate.h @@ -0,0 +1,30 @@ +// Copyright 2020 Google Inc. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef TICTACTOE_DEMO_CLASSES_APPDELEGATE_SCENE_H_ +#define TICTACTOE_DEMO_CLASSES_APPDELEGATE_SCENE_H_ + +#include "cocos2d.h" + +class AppDelegate : public cocos2d::Application { + public: + AppDelegate(); + ~AppDelegate() override; + + private: + bool applicationDidFinishLaunching() override; + void applicationDidEnterBackground() override; + void applicationWillEnterForeground() override; +}; +#endif // TICTACTOE_DEMO_CLASSES_APPDELEGATE_SCENE_H_ \ No newline at end of file diff --git a/demos/TicTacToe/game_resources/Classes/main_menu_scene.cc b/demos/TicTacToe/game_resources/Classes/main_menu_scene.cc new file mode 100644 index 00000000..3f5e9312 --- /dev/null +++ b/demos/TicTacToe/game_resources/Classes/main_menu_scene.cc @@ -0,0 +1,1087 @@ +// Copyright 2020 Google Inc. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "main_menu_scene.h" + +#include +#include + +#include "cocos/ui/UIButton.h" +#include "cocos/ui/UITextField.h" +#include "cocos2d.h" +#include "firebase/auth.h" +#include "firebase/database.h" +#include "firebase/future.h" +#include "firebase/util.h" +#include "tic_tac_toe_scene.h" +#include "util.h" + +using cocos2d::CallFunc; +using cocos2d::Color3B; +using cocos2d::Color4B; +using cocos2d::Color4F; +using cocos2d::DelayTime; +using cocos2d::Director; +using cocos2d::DrawNode; +using cocos2d::Event; +using cocos2d::EventListenerTouchOneByOne; +using cocos2d::Label; +using cocos2d::Menu; +using cocos2d::MenuItem; +using cocos2d::MenuItemSprite; +using cocos2d::RepeatForever; +using cocos2d::Scene; +using cocos2d::Sequence; +using cocos2d::Size; +using cocos2d::Sprite; +using cocos2d::TextFieldTTF; +using cocos2d::TextHAlignment; +using cocos2d::Touch; +using cocos2d::Vec2; +using cocos2d::ui::Button; +using cocos2d::ui::TextField; +using cocos2d::ui::Widget; +using firebase::App; +using firebase::InitResult; +using firebase::kFutureStatusComplete; +using firebase::ModuleInitializer; +using firebase::auth::Auth; +using firebase::auth::kAuthErrorNone; +using firebase::database::Database; +using std::array; +using std::string; +using std::to_string; + +// States for button images. +static const enum kImageState { kNormal, kPressed }; + +// Panel image filenames. +static const char* kSignUpPanelImage = "sign_up_panel.png"; +static const char* kGameMenuPanelImage = "game_menu_panel.png"; +static const char* kAuthPanelImage = "auth_panel.png"; +static const char* kLoginPanelImage = "login_panel.png"; +static const char* kUserRecordPanelImage = "user_record_panel.png"; + +// Button image filenames. +static const array kCreateGameButton = {"create_game.png", + "create_game_dark.png"}; +static const array kJoinButton = {"join_game.png", + "join_game_dark.png"}; +static const array kLoginButton = {"login.png", "login_dark.png"}; +static const array kLogoutButton = {"logout.png", "logout_dark.png"}; +static const array kBackButton = {"leave.png", "leave_dark.png"}; +static const array kSignUpButton = {"sign_up.png", + "sign_up_dark.png"}; +static const array kSkipButton = {"skip.png", "skip_dark.png"}; +static const array kLeaveAnonButton = {"leave_anon.png", + "leave_anon_dark.png"}; + +// Text box filenames. +static const char* kTextFieldOneImage = "text_field_grey.png"; +static const char* kTextFieldTwoImage = "text_field_white.png"; + +static const char* kBackgroundImage = "background.png"; +static const char* kLoadingBackgroundImage = "loading_background.png"; + +// Regex that will validate if the email entered is a valid email pattern. +const std::regex email_pattern("(\\w+)(\\.|_)?(\\w*)@(\\w+)(\\.(\\w+))+"); + +Scene* MainMenuScene::createScene() { + // Builds a simple scene that uses the bottom left cordinate point as (0,0) + // and can have sprites, labels and layers added onto it. + auto scene = Scene::create(); + auto layer = MainMenuScene::create(); + scene->addChild(layer); + + return scene; +} + +bool MainMenuScene::init() { + if (!Layer::init()) { + return false; + } + + // Initializes the firebase features. + this->InitializeFirebase(); + + // Initializes the loading layer by setting the background that expires on a + // delay. + this->InitializeLoadingLayer(); + + // Initializes the authentication layer by creating the background and placing + // all required cocos2d components. + this->InitializeAuthenticationLayer(); + + // Initializes the login layer by creating the background and placing all + // required cocos2d components. + this->InitializeLoginLayer(); + + // Initializes the login layer by creating the background and placing all + // required cocos2d components. + this->InitializeSignUpLayer(); + + // Initializes the game menu layer by creating the background and placing all + // required cocos2d components. + this->InitializeGameMenuLayer(); + + // Kicks off the updating game loop. + this->scheduleUpdate(); + + return true; +} + +// Initialize the firebase auth and database while also ensuring no +// dependencies are missing. +void MainMenuScene::InitializeFirebase() { + LogMessage("Initialize Firebase App."); + firebase::App* app; +#if defined(_ANDROID_) + app = firebase::App::Create(GetJniEnv(), GetActivity()); +#else + app = firebase::App::Create(); +#endif // defined(ANDROID) + + LogMessage("Initialize Firebase Auth and Firebase Database."); + + // Use ModuleInitializer to initialize both Auth and Database, ensuring no + // dependencies are missing. + database_ = nullptr; + auth_ = nullptr; + void* initialize_targets[] = {&auth_, &database_}; + + const firebase::ModuleInitializer::InitializerFn initializers[] = { + [](firebase::App* app, void* data) { + LogMessage("Attempt to initialize Firebase Auth."); + void** targets = reinterpret_cast(data); + firebase::InitResult result; + *reinterpret_cast(targets[0]) = + firebase::auth::Auth::GetAuth(app, &result); + return result; + }, + [](firebase::App* app, void* data) { + LogMessage("Attempt to initialize Firebase Database."); + void** targets = reinterpret_cast(data); + firebase::InitResult result; + *reinterpret_cast(targets[1]) = + firebase::database::Database::GetInstance(app, &result); + return result; + }}; + + firebase::ModuleInitializer initializer; + initializer.Initialize(app, initialize_targets, initializers, + sizeof(initializers) / sizeof(initializers[0])); + + WaitForCompletion(initializer.InitializeLastResult(), "Initialize"); + + if (initializer.InitializeLastResult().error() != 0) { + LogMessage("Failed to initialize Firebase libraries: %s", + initializer.InitializeLastResult().error_message()); + ProcessEvents(2000); + } + LogMessage("Successfully initialized Firebase Auth and Firebase Database."); + + database_->set_persistence_enabled(true); +} + +// 1. Adds the user record label (wins, loses & ties). +// 2. Creates the background for the node. +// 3. Adds the join and create button. +// 4. Adds the enter code text field and their event listeners. +// 5. Adds the logout button. +void MainMenuScene::InitializeGameMenuLayer() { + // Creates the game menu background. + game_menu_background_ = this->CreateBackground(kBackgroundImage); + game_menu_background_->setVisible(false); + this->addChild(game_menu_background_); + + // Creates and places the panel on the background. + const auto game_menu_panel_origin = Vec2(300, 295); + const auto game_menu_panel = Sprite::create(kGameMenuPanelImage); + game_menu_panel->setPosition(game_menu_panel_origin); + game_menu_background_->addChild(game_menu_panel, /*layer_index=*/10); + + // Creates the user record panel. + const auto user_record_panel = Sprite::create(kUserRecordPanelImage); + user_record_panel->setPosition(Vec2(405, 575)); + game_menu_background_->addChild(user_record_panel); + + // Label to display the user's wins. + user_record_wins_ = + Label::createWithTTF("", "fonts/GoogleSans-Regular.ttf", 28); + user_record_wins_->setTextColor(Color4B::GRAY); + user_record_wins_->setPosition(Vec2(88, 33)); + user_record_panel->addChild(user_record_wins_); + + // Label to display the user's losses. + user_record_loses_ = + Label::createWithTTF("", "fonts/GoogleSans-Regular.ttf", 28); + user_record_loses_->setTextColor(Color4B::GRAY); + user_record_loses_->setPosition(Vec2(180, 33)); + user_record_panel->addChild(user_record_loses_); + + // Label to display the user's ties. + user_record_ties_ = + Label::createWithTTF("", "fonts/GoogleSans-Regular.ttf", 28); + user_record_ties_->setTextColor(Color4B::GRAY); + user_record_ties_->setPosition(Vec2(280, 33)); + user_record_panel->addChild(user_record_ties_); + + // Creates the join_text_field. + auto join_text_field_position = Vec2(394, 80); + auto join_text_field_size = Size(180, 80); + auto join_text_field = + TextField::create("code", "fonts/GoogleSans-Regular.ttf", 48); + join_text_field->setTextColor(Color4B::GRAY); + join_text_field->setPosition(join_text_field_position); + join_text_field->setTouchSize(join_text_field_size); + join_text_field->setTouchAreaEnabled(true); + join_text_field->setMaxLength(/*max_characters=*/4); + join_text_field->setMaxLengthEnabled(true); + game_menu_panel->addChild(join_text_field, /*layer_index=*/1); + + // Adds the event listener to handle the actions for the text field. + join_text_field->addEventListener([this](Ref* sender, + TextField::EventType type) { + auto join_text_field = dynamic_cast(sender); + string join_text_field_string = join_text_field->getString(); + // Transforms the letter casing to uppercase. + std::transform(join_text_field_string.begin(), join_text_field_string.end(), + join_text_field_string.begin(), toupper); + switch (type) { + case TextField::EventType::ATTACH_WITH_IME: + // Adds the repeated blinking cursor action. + CreateBlinkingCursorAction(join_text_field); + break; + case TextField::EventType::DETACH_WITH_IME: + // Stops the blinking cursor. + join_text_field->stopAllActions(); + break; + case TextField::EventType::INSERT_TEXT: + join_text_field->setString(join_text_field_string); + break; + default: + break; + } + }); + + // Creates the background text box for the join_text_field_. + const auto join_text_field_background = Sprite::create(kTextFieldTwoImage); + join_text_field_background->setScale(1.47f); + join_text_field_background->setPosition(join_text_field_position); + game_menu_panel->addChild(join_text_field_background, /*layer_index=*/0); + + // Creates the create_button. + auto create_button = + Button::create(kCreateGameButton[kNormal], kCreateGameButton[kPressed]); + create_button->setPosition(Vec2(271, 205)); + game_menu_panel->addChild(create_button); + + // Adds the event listener to swap scenes to the TicTacToe scene. + create_button->addTouchEventListener( + [this, join_text_field](Ref* sender, Widget::TouchEventType type) { + switch (type) { + case Widget::TouchEventType::ENDED: + Director::getInstance()->pushScene( + TicTacToe::createScene(std::string(), database_, user_uid_)); + join_text_field->setString(""); + state_ = kRunGameState; + break; + default: + break; + } + }); + + // Creates a sprite for the back button. + back_button_ = + Button::create(kLeaveAnonButton[kNormal], kLeaveAnonButton[kPressed]); + back_button_->setPosition(Vec2(120, 575)); + game_menu_background_->addChild(back_button_); + back_button_->setVisible(false); + + // Adds the event listener to change to the kAuthMenuState. + back_button_->addTouchEventListener( + [this, join_text_field](Ref* sender, Widget::TouchEventType type) { + switch (type) { + case Widget::TouchEventType::ENDED: + ClearAuthFields(); + user_result_.Release(); + user_->Delete(); + RemoveUserUid(user_uid_); + user_uid_ = ""; + back_button_->setVisible(false); + state_ = kAuthMenuState; + break; + default: + break; + } + }); + + // Creates a sprite for the logout button. + logout_button_ = + Button::create(kLogoutButton[kNormal], kLogoutButton[kPressed]); + logout_button_->setPosition(Vec2(120, 575)); + game_menu_background_->addChild(logout_button_); + logout_button_->setVisible(false); + + // Adds the event listener to change to the kAuthMenuState. + logout_button_->addTouchEventListener( + [this, join_text_field](Ref* sender, Widget::TouchEventType type) { + switch (type) { + case Widget::TouchEventType::ENDED: + ClearAuthFields(); + user_uid_ = ""; + user_ = nullptr; + user_result_.Release(); + logout_button_->setVisible(false); + state_ = kAuthMenuState; + break; + default: + break; + } + }); + + // Creates a sprite for the join button and sets its position to the center + // of the screen. + auto join_button = + Button::create(kJoinButton[kNormal], kJoinButton[kPressed]); + join_button->setPosition(Vec2(148, 80)); + game_menu_panel->addChild(join_button); + + // Adds the event listener to handle touch actions for the join_button. + join_button->addTouchEventListener( + [this, join_text_field](Ref* sender, Widget::TouchEventType type) { + // Get the string from join_text_field. + const std::string join_text_field_string = join_text_field->getString(); + switch (type) { + case Widget::TouchEventType::ENDED: + if (join_text_field_string.length() == 4) { + Director::getInstance()->pushScene(TicTacToe::createScene( + join_text_field_string, database_, user_uid_)); + state_ = kRunGameState; + join_text_field->setString(""); + } else { + join_text_field->setString(""); + } + break; + default: + break; + } + }); +} + +// 1. Creates the background node. +// 2. Adds the error label and layer title label: sign up. +// 3. Adds the id and password text fields and their event listeners. +// 4. Adds the back and sign up button. +void MainMenuScene::InitializeSignUpLayer() { + // Creates a background to add on all of the cocos2d components. The + // visiblity of this node should match kSignUpState and only active this + // layers event listeners. + + sign_up_background_ = this->CreateBackground(kBackgroundImage); + sign_up_background_->setVisible(false); + this->addChild(sign_up_background_); + + // Creates the sign up panel. + const auto sign_up_panel_origin = Vec2(300, 325); + const auto sign_up_panel = Sprite::create(kSignUpPanelImage); + sign_up_panel->setPosition(sign_up_panel_origin); + sign_up_background_->addChild(sign_up_panel, /*layer_index=*/10); + + // Label to output sign up errors. + sign_up_error_label_ = + Label::createWithTTF("", "fonts/GoogleSans-Regular.ttf", 20); + sign_up_error_label_->setTextColor(Color4B(255, 82, 82, 240)); + sign_up_error_label_->setPosition(Vec2(255, 310)); + sign_up_panel->addChild(sign_up_error_label_); + + // Creates the sign_up_id_ text field. + const auto id_font_size = 32; + const auto id_position = Vec2(255, 260); + const auto id_size = Size(450, id_font_size * 1.75); + sign_up_id_ = + TextField::create("Email", "fonts/GoogleSans-Regular.ttf", id_font_size); + sign_up_id_->setTextColor(Color4B::GRAY); + sign_up_id_->setPosition(id_position); + sign_up_id_->setTouchAreaEnabled(true); + sign_up_id_->setTouchSize(id_size); + sign_up_panel->addChild(sign_up_id_, /*layer_index=*/1); + + // Creates the text box background for the sign_up_id_ text field. + const auto id_background = Sprite::create(kTextFieldOneImage); + id_background->setPosition(id_position); + sign_up_panel->addChild(id_background, /*layer_index=*/0); + + // Adds the event listener to handle the actions for the sign_up_id_. + sign_up_id_->addEventListener([this](Ref* sender, TextField::EventType type) { + auto sign_up_id_ = dynamic_cast(sender); + switch (type) { + case TextField::EventType::ATTACH_WITH_IME: + // Creates and runs the repeated blinking cursor action. + CreateBlinkingCursorAction(sign_up_id_); + break; + case TextField::EventType::DETACH_WITH_IME: + // Stops the blinking cursor. + sign_up_id_->stopAllActions(); + break; + default: + break; + } + }); + + // Creates the sign_up_password_ text field. + const auto password_font_size = 32; + const auto password_position = Vec2(255, 172); + const auto password_size = Size(450, password_font_size * 1.75); + sign_up_password_ = TextField::create( + "Password", "fonts/GoogleSans-Regular.ttf", password_font_size); + sign_up_password_->setTextColor(Color4B::GRAY); + sign_up_password_->setPosition(password_position); + sign_up_password_->setTouchAreaEnabled(true); + sign_up_password_->setTouchSize(password_size); + sign_up_password_->setPasswordEnabled(true); + sign_up_panel->addChild(sign_up_password_, /*layer_index=*/1); + + // Creates the text box background for the sign_up_password_ text field. + const auto password_background = Sprite::create(kTextFieldOneImage); + password_background->setPosition(password_position); + sign_up_panel->addChild(password_background, /*layer_index=*/0); + + // Adds the event listener to handle the actions for the sign_up_password_ + // text field. + sign_up_password_->addEventListener( + [this](Ref* sender, TextField::EventType type) { + auto sign_up_password_ = dynamic_cast(sender); + switch (type) { + case TextField::EventType::ATTACH_WITH_IME: + // Creates and runs the repeated blinking cursor action. + CreateBlinkingCursorAction(sign_up_password_); + break; + case TextField::EventType::DETACH_WITH_IME: + // Stops the blinking cursor. + sign_up_password_->stopAllActions(); + break; + default: + break; + } + }); + + // Creates the password_confirm text field. + const auto password_confirm_font_size = 32; + const auto password_confirm_position = Vec2(255, 85); + const auto password_confirm_size = + Size(450, password_confirm_font_size * 1.75); + sign_up_password_confirm_ = + TextField::create("Confirm password", "fonts/GoogleSans-Regular.ttf", + password_confirm_font_size); + sign_up_password_confirm_->setTextColor(Color4B::GRAY); + sign_up_password_confirm_->setPosition(password_confirm_position); + sign_up_password_confirm_->setTouchAreaEnabled(true); + sign_up_password_confirm_->setTouchSize(password_confirm_size); + sign_up_password_confirm_->setPasswordEnabled(true); + sign_up_panel->addChild(sign_up_password_confirm_, /*layer_index=*/1); + + // Creates the text box background for the sign_up_password_confirm_ text + // field. + const auto password_confirm_background = Sprite::create(kTextFieldOneImage); + password_confirm_background->setPosition(password_confirm_position); + sign_up_panel->addChild(password_confirm_background, /*layer_index=*/0); + + // Adds the event listener to handle the actions for the + // sign_up_password_confirm text field. + sign_up_password_confirm_->addEventListener( + [this](Ref* sender, TextField::EventType type) { + auto sign_up_password_confirm_ = dynamic_cast(sender); + switch (type) { + case TextField::EventType::ATTACH_WITH_IME: + // Creates and runs the repeated blinking cursor action. + CreateBlinkingCursorAction(sign_up_password_confirm_); + break; + case TextField::EventType::DETACH_WITH_IME: + // Stops the blinking cursor. + sign_up_password_confirm_->stopAllActions(); + break; + default: + break; + } + }); + + // Creates the sign_up_button. + auto sign_up_button = + Button::create(kSignUpButton[kNormal], kSignUpButton[kPressed]); + sign_up_button->setPosition(Vec2(255, 385)); + sign_up_panel->addChild(sign_up_button); + + // Adds the event listener to handle touch actions for the sign_up_button. + sign_up_button->addTouchEventListener( + [this](Ref* sender, Widget::TouchEventType type) { + switch (type) { + case Widget::TouchEventType::ENDED: + // Validates the id and passwords are valid, then sets the + // user_result_ future and swaps to kSignUpState. + if (!std::regex_match(sign_up_id_->getString(), email_pattern)) { + sign_up_error_label_->setString("invalid email address"); + } else if (sign_up_password_->getString().length() < 8) { + sign_up_error_label_->setString( + "password must be at least 8 characters long"); + } else if (sign_up_password_->getString() != + sign_up_password_confirm_->getString()) { + sign_up_error_label_->setString("passwords do not match"); + } else { + // Clears error label and sets user_result_ to the future created + // user. + sign_up_error_label_->setString(""); + user_result_ = auth_->CreateUserWithEmailAndPassword( + sign_up_id_->getString().c_str(), + sign_up_password_->getString().c_str()); + + // Sets the state to kSignUpState. + state_ = kSignUpState; + } + break; + default: + break; + } + }); + + // Creates the return button. + auto return_button = + Button::create(kBackButton[kNormal], kBackButton[kPressed]); + return_button->setScale(.3); + return_button->setPosition(Size(50, 450)); + sign_up_panel->addChild(return_button); + + // Adds the event listener to return to kAuthMenuState. + return_button->addTouchEventListener( + [this](Ref* sender, Widget::TouchEventType type) { + switch (type) { + case Widget::TouchEventType::ENDED: + // Clears all of the labels and text fields before swaping state. + this->ClearAuthFields(); + state_ = kAuthMenuState; + break; + default: + break; + } + }); +} + +// 1. Creates the background node. +// 2. Adds the error label and layer title label: login. +// 3. Adds the id and password text fields and their event listeners. +// 4. Adds the back and login button. +void MainMenuScene::InitializeLoginLayer() { + // Creates the game menu background. + login_background_ = this->CreateBackground(kBackgroundImage); + login_background_->setVisible(false); + this->addChild(login_background_); + + // Creates the login panel. + const auto login_panel_origin = Vec2(300, 325); + const auto login_panel = Sprite::create(kLoginPanelImage); + login_panel->setPosition(login_panel_origin); + login_background_->addChild(login_panel, /*layer_index=*/10); + + // Label to output login errors. + login_error_label_ = + Label::createWithTTF("", "fonts/GoogleSans-Regular.ttf", 24); + login_error_label_->setTextColor(Color4B(255, 82, 82, 240)); + login_error_label_->setPosition(Vec2(255, 225)); + login_panel->addChild(login_error_label_); + + // Creating the login_id_ text field. + const auto id_font_size = 32; + const auto id_position = Vec2(255, 172); + const auto id_size = Size(450, id_font_size * 1.75); + login_id_ = + TextField::create("Email", "fonts/GoogleSans-Regular.ttf", id_font_size); + login_id_->setTextColor(Color4B::GRAY); + login_id_->setPosition(id_position); + login_id_->setTouchAreaEnabled(true); + login_id_->setTouchSize(id_size); + login_panel->addChild(login_id_, /*layer_index=*/1); + + // Creates the text box background for the login id text field. + auto id_background = Sprite::create(kTextFieldOneImage); + id_background->setPosition(id_position); + login_panel->addChild(id_background, /*layer_index=*/0); + + // Adds the event listener to handle the actions for the login_id_. + login_id_->addEventListener([this](Ref* sender, TextField::EventType type) { + auto login_id_ = dynamic_cast(sender); + switch (type) { + case TextField::EventType::ATTACH_WITH_IME: + // Creates and runs the repeated blinking cursor action. + CreateBlinkingCursorAction(login_id_); + break; + case TextField::EventType::DETACH_WITH_IME: + // Stops the blinking cursor. + login_id_->stopAllActions(); + break; + default: + break; + } + }); + + // Creates the login_password_ text field. + const auto password_font_size = 32; + const auto password_position = Vec2(255, 75); + const auto password_size = Size(450, password_font_size * 1.75); + login_password_ = TextField::create( + "Password", "fonts/GoogleSans-Regular.ttf", password_font_size); + login_password_->setTextColor(Color4B::GRAY); + login_password_->setPosition(password_position); + login_password_->setTouchAreaEnabled(true); + login_password_->setTouchSize(password_size); + login_password_->setPasswordEnabled(true); + login_panel->addChild(login_password_, /*layer_index=*/1); + + // Creates the text box background for the login password text field. + auto password_background = Sprite::create(kTextFieldOneImage); + password_background->setPosition(password_position); + login_panel->addChild(password_background, /*layer_index=*/0); + + // Adds the event listener to handle the actions for the login_password_ text + // field. + login_password_->addEventListener( + [this](Ref* sender, TextField::EventType type) { + auto login_password_ = dynamic_cast(sender); + switch (type) { + case TextField::EventType::ATTACH_WITH_IME: + // Creates and runs the repeated blinking cursor action. + CreateBlinkingCursorAction(login_password_); + break; + case TextField::EventType::DETACH_WITH_IME: + // Stops the blinking cursor. + login_password_->stopAllActions(); + break; + default: + break; + } + }); + + // Creates the login_button. + auto login_button = + Button::create(kLoginButton[kNormal], kLoginButton[kPressed]); + login_button->setPosition(Size(255, 300)); + login_panel->addChild(login_button); + + // Adds the event listener to handle touch actions for the login_button. + login_button->addTouchEventListener( + [this](Ref* sender, Widget::TouchEventType type) { + switch (type) { + case Widget::TouchEventType::ENDED: + // Validates the id and passwords are valid, then sets the + // user_result_ future. + if (!std::regex_match(login_id_->getString(), email_pattern)) { + login_error_label_->setString("invalid email address"); + } else if (login_password_->getString().length() < 8) { + login_error_label_->setString( + "password must be at least 8 characters long"); + } else { + // Clears error label and sets user_result_ to the future existing + // user. + login_error_label_->setString(""); + user_result_ = auth_->SignInWithEmailAndPassword( + login_id_->getString().c_str(), + login_password_->getString().c_str()); + } + break; + default: + break; + } + }); + + // Creates the return button. + auto return_button = + Button::create(kBackButton[kNormal], kBackButton[kPressed]); + return_button->setScale(.3); + return_button->setPosition(Size(50, 375)); + login_panel->addChild(return_button); + + // Adds the event listener to return to kAuthMenuState. + return_button->addTouchEventListener( + [this](Ref* sender, Widget::TouchEventType type) { + switch (type) { + case Widget::TouchEventType::ENDED: + // Clears all of the labels and text fields before swaping state. + this->ClearAuthFields(); + state_ = kAuthMenuState; + break; + default: + break; + } + }); +} + +// Creates and places the loading background. Creates a action sequence to delay +// then swap to the authentication state. +void MainMenuScene::InitializeLoadingLayer() { + // Creates the delay action. + auto loading_delay = DelayTime::create(/*delay_durration*/ 2.0f); + + // Creates a callback function to swap state to kAuthMenuState. + auto SwapToAuthState = + CallFunc::create([this]() { state_ = kAuthMenuState; }); + + // Runs the sequence that will delay followed by the swap state callback + // function. + this->runAction(Sequence::create(loading_delay, SwapToAuthState, NULL)); + + // Creates the loading background sprite. + loading_background_ = this->CreateBackground(kLoadingBackgroundImage); + loading_background_->setContentSize(Size(600, 641)); + this->addChild(loading_background_); +} + +// 1. Creates the background node. +// 2. Adds the layer title label: authentication. +// 3. Adds the id and password text fields and their event listeners. +// 4. Adds the back and login button. +void MainMenuScene::InitializeAuthenticationLayer() { + // Creates the auth_background. + auth_background_ = this->CreateBackground(kBackgroundImage); + auth_background_->setVisible(false); + this->addChild(auth_background_); + + // Creates the auth panel. + const auto auth_panel_origin = Vec2(300, 315); + const auto auth_panel = Sprite::create(kAuthPanelImage); + auth_panel->setPosition(auth_panel_origin); + auth_background_->addChild(auth_panel, /*layer_index=*/10); + + // Creates three buttons for the menu items (login,sign up, and anonymous sign + // in). + // For each menu item button, creates a normal and selected version and attach + // the touch listener. + + // Creates the sign up menu item. + const auto sign_up_normal_item = Sprite::create(kSignUpButton[kNormal]); + const auto sign_up_selected = Sprite::create(kSignUpButton[kPressed]); + + auto sign_up_item = MenuItemSprite::create( + sign_up_normal_item, sign_up_selected, [this](Ref* sender) { + auto node = dynamic_cast(sender); + if (node != nullptr) { + state_ = kSignUpState; + } + }); + sign_up_item->setTag(0); + + // Creates the login menu item. + const auto login_normal_item = Sprite::create(kLoginButton[kNormal]); + const auto login_selected_item = Sprite::create(kLoginButton[kPressed]); + + auto login_item = MenuItemSprite::create( + login_normal_item, login_selected_item, [this](Ref* sender) { + auto node = dynamic_cast(sender); + if (node != nullptr) { + state_ = kLoginState; + } + }); + login_item->setTag(1); + + // Creates the skip login menu item. + const auto skip_login_normal_item = Sprite::create(kSkipButton[kNormal]); + const auto skip_login_selected_item = Sprite::create(kSkipButton[kPressed]); + + auto skip_login_item = MenuItemSprite::create( + skip_login_normal_item, skip_login_selected_item, [this](Ref* sender) { + auto node = dynamic_cast(sender); + if (node != nullptr) { + user_result_ = auth_->SignInAnonymously(); + state_ = kSkipLoginState; + } + }); + skip_login_item->setTag(2); + + // Combines the individual items to create the menu. + cocos2d::Vector menuItems = {sign_up_item, login_item, + skip_login_item}; + auto menu = Menu::createWithArray(menuItems); + menu->setPosition(Size(200, 245)); + menu->setContentSize(Size(100, 200)); + menu->alignItemsVerticallyWithPadding(30.0f); + auth_panel->addChild(menu); +} + +// Gets the user record variables to reflect what is in the database. +void MainMenuScene::GetUserRecord() { + ref_ = database_->GetReference("users").Child(user_uid_); + auto future_wins = ref_.Child("wins").GetValue(); + auto future_loses = ref_.Child("loses").GetValue(); + auto future_ties = ref_.Child("ties").GetValue(); + WaitForCompletion(future_wins, "getUserWinsData"); + WaitForCompletion(future_loses, "getUserLosesData"); + WaitForCompletion(future_ties, "getUserTiesData"); + user_wins_ = future_wins.result()->value().int64_value(); + user_loses_ = future_loses.result()->value().int64_value(); + user_ties_ = future_ties.result()->value().int64_value(); +} + +// Sets the user record variables to reflect the user record local variables. +void MainMenuScene::SetUserRecord() { + ref_ = database_->GetReference("users").Child(user_uid_); + auto future_wins = ref_.Child("wins").SetValue(user_wins_); + auto future_loses = ref_.Child("loses").SetValue(user_loses_); + auto future_ties = ref_.Child("ties").SetValue(user_ties_); + WaitForCompletion(future_wins, "setUserWinsData"); + WaitForCompletion(future_loses, "setUserLosesData"); + WaitForCompletion(future_ties, "setUserTiesData"); +} + +// Clears the user record. +void MainMenuScene::ClearUserRecord() { + user_wins_ = 0; + user_loses_ = 0; + user_ties_ = 0; +} + +// Displays the user record. +void MainMenuScene::DisplayUserRecord() { + user_record_wins_->setString(to_string(user_wins_)); + user_record_loses_->setString(to_string(user_loses_)); + user_record_ties_->setString(to_string(user_ties_)); +} + +// Overriding the onEnter method to update the user_record on reenter. +void MainMenuScene::onEnter() { + // If the scene is entering from the game, UpdateUserRecords() and change + // state_ back to kGameMenuState. + if (state_ == kRunGameState) { + this->GetUserRecord(); + this->DisplayUserRecord(); + state_ = kGameMenuState; + } + Layer::onEnter(); +} + +// Clears all of the labels and text fields on the login and sign up layers. +void MainMenuScene::ClearAuthFields() { + // Clears the login components. + login_id_->setString(""); + login_password_->setString(""); + login_error_label_->setString(""); + + // Clears the sign up components. + sign_up_id_->setString(""); + sign_up_password_->setString(""); + sign_up_password_confirm_->setString(""); + sign_up_error_label_->setString(""); +} + +// Removes the user_uid from the games database. +void MainMenuScene::RemoveUserUid(const string& user_uid) { + WaitForCompletion( + database_->GetReference("users").Child(user_uid).RemoveValue(), + "removeUserUid"); +} + +// Updates every frame: +// +// switch (state_) +// (0) kInitializingState: swaps to (1). +// (1) kAuthMenuState: makes the auth_background_ visable. +// (2) kGameMenuState: makes the game_menu_background_ invisable. +// (3) kSkipLoginState: waits for anonymous sign in then swaps to (2). +// (4) kSignUpState: waits for sign up future completion, +// updates user variables, and swaps to (2). +// (5) kLoginState: waits for login future completion, +// updates user variables, and swaps to (2). +// (6) kRunGameState: waits for director to pop the TicTacToeScene. +void MainMenuScene::update(float /*delta*/) { + switch (state_) { + case kInitializingState: + state_ = UpdateInitialize(); + break; + case kAuthMenuState: + state_ = UpdateAuthentication(); + break; + case kGameMenuState: + state_ = UpdateGameMenu(); + break; + case kSkipLoginState: + state_ = UpdateSkipLogin(); + break; + case kSignUpState: + state_ = UpdateSignUp(); + break; + case kLoginState: + state_ = UpdateLogin(); + break; + case kRunGameState: + state_ = UpdateRunGame(); + break; + default: + assert(0); + } +} + +// Returns kInitializingState. Waits for the delay action sequence to callback +// SwaptoAuthState() to set state_ = kAuthMenuState. +MainMenuScene::kSceneState MainMenuScene::UpdateInitialize() { + this->UpdateLayer(state_); + return kInitializingState; +} + +// Updates the layer and returns the kAuthMenuState. +MainMenuScene::kSceneState MainMenuScene::UpdateAuthentication() { + this->UpdateLayer(state_); + return kAuthMenuState; +} + +// Updates the layer and stays in this state until user_result_ completes. +// Updates the user variables if the user_result_ is valid. Updates the error +// message and returns back to kLoginState if the future user_result_ errored. +MainMenuScene::kSceneState MainMenuScene::UpdateLogin() { + this->UpdateLayer(state_); + if (user_result_.status() == firebase::kFutureStatusComplete) { + if (user_result_.error() == firebase::auth::kAuthErrorNone) { + // Updates the user to refect the uid and record (wins,losses and ties) + // stored for the user in the database. + user_ = *user_result_.result(); + user_uid_ = user_->uid(); + this->ClearAuthFields(); + this->GetUserRecord(); + this->DisplayUserRecord(); + + // Shows the logout button because the user logged in. + logout_button_->setVisible(true); + + return kGameMenuState; + } else { + // Changes login_error_label_ to display the user_result_ future + // errored. + login_error_label_->setString("invalid credentials"); + user_result_.Release(); + return kLoginState; + } + } else { + return kLoginState; + } +} + +// Updates the layer and stays in this state until user_result_ completes. +// Initializes the user if the user_result_ is valid. Updates the error +// message and returns back to kSignUpState if the future user_result_ +// errored. +MainMenuScene::kSceneState MainMenuScene::UpdateSignUp() { + this->UpdateLayer(state_); + if (user_result_.status() == firebase::kFutureStatusComplete) { + if (user_result_.error() == firebase::auth::kAuthErrorNone) { + // Initializes user variables and stores them in the database. + user_ = *user_result_.result(); + user_uid_ = user_->uid(); + this->ClearUserRecord(); + this->SetUserRecord(); + this->DisplayUserRecord(); + + // Shows the logout button because the user signed up. + logout_button_->setVisible(true); + + return kGameMenuState; + } else { + // Changes sign_up_error_label_ to display the user_result_ future + // errored. + sign_up_error_label_->setString("sign up failed"); + user_result_.Release(); + return kSignUpState; + } + } else { + return kSignUpState; + } +} + +// Updates the layer and stays in this state until user_result_ completes. +// Initializes the user if the user_result_ is valid. Otherwise, return back +// to kAuthMenuState. +MainMenuScene::kSceneState MainMenuScene::UpdateSkipLogin() { + if (user_result_.status() == firebase::kFutureStatusComplete) { + if (user_result_.error() == firebase::auth::kAuthErrorNone) { + // Initializes user variables and stores them in the database. + user_ = *user_result_.result(); + user_uid_ = GenerateUid(10); + this->ClearUserRecord(); + this->SetUserRecord(); + this->DisplayUserRecord(); + + // Shows the back button because the user skipped login. + back_button_->setVisible(true); + + return kGameMenuState; + } else { + CCLOG("Error skipping login."); + return kAuthMenuState; + } + + } else { + return kSkipLoginState; + } +} + +// Updates the layer and returns kGameMenuState. +MainMenuScene::kSceneState MainMenuScene::UpdateGameMenu() { + this->UpdateLayer(state_); + return kGameMenuState; +} + +// Continues to return that you are in the kRunGameState. +MainMenuScene::kSceneState MainMenuScene::UpdateRunGame() { + return kRunGameState; +} + +// Returns a repeating action that toggles the cursor of the text field passed +// in based on the toggle_delay. +void MainMenuScene::CreateBlinkingCursorAction( + cocos2d::ui::TextField* text_field) { + // Creates a callable function that shows the cursor and sets the cursor + // character. + const auto show_cursor = CallFunc::create([text_field]() { + text_field->setCursorEnabled(true); + text_field->setCursorChar('|'); + }); + + // Creates a callable function that hides the cursor character. + const auto hide_cursor = + CallFunc::create([text_field]() { text_field->setCursorChar(' '); }); + + // Creates a delay action. + const cocos2d::DelayTime* delay = DelayTime::create(/*delay_durration=*/0.3f); + + // Aligns the sequence of actions to create a blinking cursor. + auto blink_cursor_action = + Sequence::create(show_cursor, delay, hide_cursor, delay, nullptr); + + // Creates a forever repeating action based on the blink_cursor_action. + text_field->runAction(RepeatForever::create(blink_cursor_action)); +} + +// Creates a background the same size as the window and places it to cover the +// entire window. +Sprite* MainMenuScene::CreateBackground(const string& background_image) { + const auto window_size = Director::getInstance()->getWinSize(); + const auto background = Sprite::create(background_image); + background->setContentSize(window_size); + background->setAnchorPoint(Vec2(0, 0)); + return background; +} + +// Updates the auth_,login_, sign_up_, and game_menu_ layer based on state. +void MainMenuScene::UpdateLayer(MainMenuScene::kSceneState state) { + auth_background_->setVisible(state == kAuthMenuState); + login_background_->setVisible(state == kLoginState); + sign_up_background_->setVisible(state == kSignUpState); + game_menu_background_->setVisible(state == kGameMenuState); + loading_background_->setVisible(state == kInitializingState); +} \ No newline at end of file diff --git a/demos/TicTacToe/game_resources/Classes/main_menu_scene.h b/demos/TicTacToe/game_resources/Classes/main_menu_scene.h new file mode 100644 index 00000000..e8342370 --- /dev/null +++ b/demos/TicTacToe/game_resources/Classes/main_menu_scene.h @@ -0,0 +1,155 @@ +// Copyright 2020 Google Inc. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef TICTACTOE_DEMO_CLASSES_MAINMENU_SCENE_H_ +#define TICTACTOE_DEMO_CLASSES_MAINMENU_SCENE_H_ + +#include + +#include "cocos/ui/UIButton.h" +#include "cocos/ui/UITextField.h" +#include "cocos2d.h" +#include "firebase/auth.h" +#include "firebase/database.h" +#include "firebase/future.h" + +class MainMenuScene : public cocos2d::Layer, public cocos2d::TextFieldDelegate { + public: + // Build a simple scene that uses the bottom left cordinate point as (0,0) + // and can have sprites, labels and nodes added onto it. + static cocos2d::Scene* createScene(); + + private: + // Defines the state the class is currently in, which updates the sprites in + // the MainMenuScene::update(float) method. + enum kSceneState { + kInitializingState, + kAuthMenuState, + kLoginState, + kSignUpState, + kGameMenuState, + kSkipLoginState, + kRunGameState + }; + + // Creates and runs an endless blinking cursor action for the textfield passed + // in. + void CreateBlinkingCursorAction(cocos2d::ui::TextField*); + + // Creates the background sprite image. + cocos2d::Sprite* CreateBackground(const std::string&); + + // Updates the scene to show the active layer based on state. + void UpdateLayer(MainMenuScene::kSceneState); + + // The game loop method for this layer which runs every frame once scheduled + // using this->scheduleUpdate(). Acts as the state manager for this scene. + void update(float) override; + + // Update methods to corresponding to each kSceneState. + kSceneState UpdateAuthentication(); + kSceneState UpdateGameMenu(); + kSceneState UpdateInitialize(); + kSceneState UpdateLogin(); + kSceneState UpdateSignUp(); + kSceneState UpdateSkipLogin(); + kSceneState UpdateRunGame(); + + // If the scene is re-entered from TicTacToeScene, then call + // UpdateUserRecord() and swap state_ to kGameMenuState. + void onEnter() override; + + // Set, Get, Clear and Display functions for the user record. + void SetUserRecord(); + void GetUserRecord(); + void ClearUserRecord(); + void DisplayUserRecord(); + + // Initializes the loading layer which includes a background loading image and + // state swap delay action. + void InitializeLoadingLayer(); + + // Initializes the game menu layer which includes the background, buttons + // and labels related to setting up the game menu. + void InitializeGameMenuLayer(); + + // Initializes the authentication layer which includes the background, buttons + // and labels related to authenticating the user. + void InitializeAuthenticationLayer(); + + // Initializes the sign up layer which includes the background, buttons + // and labels related to signing up the user. + void InitializeSignUpLayer(); + + // Initializes the login layer which includes the background, buttons + // and labels. + void InitializeLoginLayer(); + + // Clears the labels and text fields for all authentication layers. + void ClearAuthFields(); + + // Removes the user_uid from the database users collection. + void RemoveUserUid(const std::string&); + + // Initializes the the firebase app, auth, and database. + void InitializeFirebase(); + // Initializes the instance of a Node and returns a boolean based on if it was + // successful in doing so. + bool init() override; + CREATE_FUNC(MainMenuScene); + + // Sprites to be used as a background each state. + cocos2d::Sprite* auth_background_; + cocos2d::Sprite* login_background_; + cocos2d::Sprite* sign_up_background_; + cocos2d::Sprite* game_menu_background_; + cocos2d::Sprite* loading_background_; + + // Exit buttons for the game menu screen. Logout button is shown if the user + // logged in or signed up. Back button is shown if the user skipped login. + cocos2d::ui::Button* back_button_; + cocos2d::ui::Button* logout_button_; + + // Labels and textfields for the authentication menu. + cocos2d::Label* login_error_label_; + cocos2d::Label* sign_up_error_label_; + cocos2d::Label* user_record_wins_; + cocos2d::Label* user_record_loses_; + cocos2d::Label* user_record_ties_; + + // Cocos2d components for the login layer. + cocos2d::ui::TextField* login_id_; + cocos2d::ui::TextField* login_password_; + + // Cocos2d components for the sign up layer. + cocos2d::ui::TextField* sign_up_id_; + cocos2d::ui::TextField* sign_up_password_; + cocos2d::ui::TextField* sign_up_password_confirm_; + + kSceneState state_ = kInitializingState; + + // User record variabales that are stored in firebase database. + int user_wins_; + int user_loses_; + int user_ties_; + + std::string user_uid_; + firebase::auth::Auth* auth_; + firebase::auth::User* user_; + firebase::Future user_result_; + firebase::database::Database* database_; + firebase::database::DatabaseReference ref_; +}; + +#endif // TICTACTOE_DEMO_CLASSES_MAINMENU_SCENE_H_ \ No newline at end of file diff --git a/demos/TicTacToe/game_resources/Classes/tic_tac_toe_layer.cc b/demos/TicTacToe/game_resources/Classes/tic_tac_toe_layer.cc new file mode 100644 index 00000000..46fcbea3 --- /dev/null +++ b/demos/TicTacToe/game_resources/Classes/tic_tac_toe_layer.cc @@ -0,0 +1,607 @@ +// Copyright 2020 Google Inc. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "tic_tac_toe_layer.h" + +#include +#include +#include +#include +#include + +#include "cocos/ui/UIButton.h" +#include "cocos/ui/UIWidget.h" +#include "cocos2d.h" +#include "firebase/database.h" +#include "firebase/variant.h" +#include "util.h" + +using cocos2d::CallFunc; +using cocos2d::Color4B; +using cocos2d::DelayTime; +using cocos2d::Director; +using cocos2d::Event; +using cocos2d::EventListenerTouchOneByOne; +using cocos2d::Label; +using cocos2d::Sequence; +using cocos2d::Sprite; +using cocos2d::Touch; +using cocos2d::Vec2; +using cocos2d::ui::Button; +using cocos2d::ui::Widget; +using firebase::Variant; +using firebase::database::DataSnapshot; +using firebase::database::Error; +using firebase::database::TransactionResult; +using firebase::database::ValueListener; +using std::array; +using std::make_unique; +using std::string; +using std::vector; + +// Player constants. +static const int kEmptyTile = -1; +static const int kPlayerOne = 0; +static const int kPlayerTwo = 1; +static const int kNumberOfPlayers = 2; + +// End game outcomes. +static const enum kGameOutcome { + kGameWon = 0, + kGameLost, + kGameTied, + kGameDisbanded +}; + +// States for button images. +static const enum kImageState { kNormal, kPressed }; + +// create an array that has indicies of enum kGameOutcome and maps that to the +// database outcome key. +static const const char* kGameOutcomeStrings[] = {"wins", "loses", "ties", + "disbanded"}; +// Game board dimensions. +extern const int kTilesX; +extern const int kTilesY; +static const int kNumberOfTiles = kTilesX * kTilesY; + +// Game board dimensions. +static const double kBoardWidth = 482; +static const double kBoardHeight = 484; +static const double kBoardLineWidth = 25; +static const double kBoardLineHeight = 25; +static const double kTileWidthHitBox = (kBoardWidth / kTilesX); +static const double kTileHeightHitBox = (kBoardHeight / kTilesY); +static const double kTileWidth = + ((kBoardWidth - ((kTilesX - 1) * kBoardLineWidth)) / kTilesX); +static const double kTileHeight = + ((kBoardHeight - ((kTilesY - 1) * kBoardLineHeight)) / kTilesY); +static const Vec2 kBoardOrigin = + Vec2(300 - (kBoardWidth / 2), 312 - (kBoardHeight / 2)); + +// The screen will display the end game text for 2 seconds (120frames/60fps). +static const int kEndGameFramesMax = 120; + +// Constants for the image filenames. +static const char* kTextFieldImage = "text_field_white.png"; +static const char* kBoardImageFileName = "tic_tac_toe_board.png"; +static const array kLeaveButton = {"leave.png", "leave_dark.png"}; +static const array kGameOutcomeImages = { + "outcome_won.png", "outcome_lost.png", "outcome_tied.png", + "outcome_disbanded.png"}; +static const array kPlayerTokenFileNames = { + "tic_tac_toe_x.png", "tic_tac_toe_o.png"}; + +// An example of a ValueListener object. This specific version will +// simply log every value it sees, and store them in a list so we can +// confirm that all values were received. +class SampleValueListener : public ValueListener { + public: + void OnValueChanged(const DataSnapshot& snapshot) override { + LogMessage(" ValueListener.OnValueChanged(%s)", + snapshot.value().AsString().string_value()); + last_seen_value_ = snapshot.value(); + seen_values_.push_back(snapshot.value()); + } + void OnCancelled(const Error& error_code, + const char* error_message) override { + LogMessage("ERROR: SampleValueListener canceled: %d: %s", error_code, + error_message); + } + const Variant& last_seen_value() { return last_seen_value_; } + bool seen_value(const Variant& value) { + return std::find(seen_values_.begin(), seen_values_.end(), value) != + seen_values_.end(); + } + size_t num_seen_values() { return seen_values_.size(); } + + private: + Variant last_seen_value_; + vector seen_values_; +}; + +// An example ChildListener class. +class SampleChildListener : public firebase::database::ChildListener { + public: + void OnChildAdded(const DataSnapshot& snapshot, + const char* previous_sibling) override { + LogMessage(" ChildListener.OnChildAdded(%s)", snapshot.key()); + events_.push_back(string("added ") + snapshot.key()); + } + void OnChildChanged(const DataSnapshot& snapshot, + const char* previous_sibling) override { + LogMessage(" ChildListener.OnChildChanged(%s)", snapshot.key()); + events_.push_back(string("changed ") + snapshot.key()); + } + void OnChildMoved(const DataSnapshot& snapshot, + const char* previous_sibling) override { + LogMessage(" ChildListener.OnChildMoved(%s)", snapshot.key()); + events_.push_back(string("moved ") + snapshot.key()); + } + void OnChildRemoved(const DataSnapshot& snapshot) override { + LogMessage(" ChildListener.OnChildRemoved(%s)", snapshot.key()); + events_.push_back(string("removed ") + snapshot.key()); + } + void OnCancelled(const Error& error_code, + const char* error_message) override { + LogMessage("ERROR: SampleChildListener canceled: %d: %s", error_code, + error_message); + } + + // Get the total number of Child events this listener saw. + size_t total_events() { return events_.size(); } + + // Get the number of times this event was seen. + int num_events(const string& event) { + int count = 0; + for (int i = 0; i < events_.size(); i++) { + if (events_[i] == event) { + count++; + } + } + return count; + } + + public: + // Vector of strings that contains the events in the order in which they + // occurred. + vector events_; +}; + +// A ValueListener that expects a specific value to be set. +class ExpectValueListener : public ValueListener { + public: + explicit ExpectValueListener(Variant wait_value) + : wait_value_(wait_value.AsString()), got_value_(false) {} + void OnValueChanged(const DataSnapshot& snapshot) override { + if (snapshot.value().AsString() == wait_value_) { + got_value_ = true; + } else { + LogMessage( + "FAILURE: ExpectValueListener did not receive the expected result."); + } + } + void OnCancelled(const Error& error_code, + const char* error_message) override { + LogMessage("ERROR: ExpectValueListener canceled: %d: %s", error_code, + error_message); + } + + bool got_value() { return got_value_; } + + private: + Variant wait_value_; + bool got_value_; +}; + +// A function that returns true if any of the row +// is crossed with the same player's move +static bool RowCrossed(int board[][kTilesY]) { + for (int i = 0; i < kTilesY; i++) { + if (board[i][0] == board[i][1] && board[i][1] == board[i][2] && + board[i][0] != kEmptyTile) + return true; + } + return false; +} + +// A function that returns true if any of the column +// is crossed with the same player's move +static bool ColumnCrossed(int board[][kTilesY]) { + for (int i = 0; i < kTilesX; i++) { + if (board[0][i] == board[1][i] && board[1][i] == board[2][i] && + board[0][i] != kEmptyTile) + return (true); + } + return (false); +} + +// A function that returns true if any of the diagonal +// is crossed with the same player's move +static bool DiagonalCrossed(int board[][kTilesY]) { + if (board[0][0] == board[1][1] && board[1][1] == board[2][2] && + board[0][0] != kEmptyTile) + return (true); + + if (board[0][2] == board[1][1] && board[1][1] == board[2][0] && + board[0][2] != kEmptyTile) + return (true); + + return (false); +} + +// A function that returns true if the game is over +// else it returns a false +static bool GameOver(int board[][kTilesY]) { + return (RowCrossed(board) || ColumnCrossed(board) || DiagonalCrossed(board)); +} + +// Creates all of the layer's components, joins game based on game_uid existing +// in the database. +TicTacToeLayer::TicTacToeLayer(string game_uid, + firebase::database::Database* main_menu_database, + string main_menu_user) { + // Initializes starting game variables. + join_game_uid_ = game_uid; + current_player_index_ = kPlayerOne; + game_outcome_ = kGameWon; + database_ = main_menu_database; + user_uid_ = main_menu_user; + + // Sets the initial values for the player based on join_game_uid's existence. + this->InitializePlayerData(); + + // Initializes the board and cocos2d board components. + this->InitializeBoard(); + + // Initializes the SampleValue and ExpectValue listeners. + this->InitializeDatabaseListeners(); + + // Schedules the update method for this scene. + this->scheduleUpdate(); +} + +// Called automatically every frame. The update is scheduled at the end of +// the constructor. +void TicTacToeLayer::update(float /*delta*/) { + // Pops the scene if the initialization fails. + if (initialization_failed_) { + Director::getInstance()->popScene(); + } + // Performs the actions of the other player when the + // current_player_index_listener_ is equal to the player index. + else if (current_player_index_listener_->last_seen_value() == player_index_ && + awaiting_opponenet_move_ == true) { + this->UpdateBoard(); + } + // Shows the end game label for kEndGameFramesMax to show the result of the + // game. + else if (game_over_listener_->got_value() && !displaying_outcome_) { + this->DisplayGameOutcome(); + } + // Updates the waiting label to show its your move. + else if (total_player_listener_->got_value() && + awaiting_opponenet_move_ == false) { + waiting_label_->setString("Your Move"); + } +} +// Creates all of the cocos2d componets and places them on the layer. +// Initializes game board. +void TicTacToeLayer::InitializeBoard() { + // Creates the layer background color. + const auto background = + cocos2d::LayerColor::create(Color4B(255, 255, 255, 255)); + this->addChild(background); + + // Creates the game board. + board_sprite_ = Sprite::create(kBoardImageFileName); + board_sprite_->setPosition(Vec2(300, 285)); + + // Creates the leave button. + leave_button_ = Button::create(kLeaveButton[kNormal], kLeaveButton[kPressed]); + leave_button_->setPosition(Vec2(130, 595)); + this->addChild(leave_button_, /*layer_index=*/1); + + leave_button_->addTouchEventListener( + [this](Ref* sender, Widget::TouchEventType type) { + switch (type) { + case Widget::TouchEventType::ENDED: + // Update the game_outcome_ to reflect if the you rage quit or + // left pre-match. + if (remaining_tiles_.size() == kNumberOfTiles) { + game_outcome_ = kGameDisbanded; + } else { + game_outcome_ = kGameLost; + } + + WaitForCompletion(ref_.Child("game_over").SetValue(true), + "setGameOver"); + break; + default: + break; + } + }); + + // Creates the label for the game uid. + const auto game_uid_position = Vec2(470, 595); + Label* game_uid_label = + Label::createWithTTF(join_game_uid_, "fonts/GoogleSans-Regular.ttf", 30); + game_uid_label->setTextColor(Color4B(0, 0, 0, 100)); + game_uid_label->setPosition(game_uid_position); + this->addChild(game_uid_label, /*layer_index=*/1); + + // Creates the text box background for the game uid label. + const auto game_uid_background = Sprite::create(kTextFieldImage); + game_uid_background->setPosition(game_uid_position); + this->addChild(game_uid_background, /*layer_index=*/0); + + // Creates the label that displays "Waiting" or "Your Move" depending on + // current_player_. + waiting_label_ = + Label::createWithTTF("Waiting", "fonts/GoogleSans-Regular.ttf", 30); + waiting_label_->setTextColor(Color4B(255, 82, 82, 240)); + waiting_label_->setPosition(Vec2(300, 595)); + this->addChild(waiting_label_, /*layer_index=*/1); + + // Set up a 3*3 Tic-Tac-Toe board for tracking results. + for (int i = 0; i < kTilesY; i++) { + for (int j = 0; j < kTilesX; j++) { + board[i][j] = kEmptyTile; + remaining_tiles_.insert((i * kTilesX) + j); + }; + } + + // Add a function to determine which tile was selected to the onTouchBegan + // listener. + auto board_touch_listener = EventListenerTouchOneByOne::create(); + board_touch_listener->onTouchBegan = [this](Touch* touch, + Event* event) mutable -> bool { + if (!total_player_listener_->got_value()) return true; + if (current_player_index_listener_->last_seen_value() != player_index_) + return true; + + const auto bounds = event->getCurrentTarget()->getBoundingBox(); + + // Check to make sure the touch location is within the bounds of the + // board. + if (bounds.containsPoint(touch->getLocation())) { + // Calculates the tile number [0-8] which corresponds to the touch + // location. + const auto touch_board_location = + Vec2(touch->getLocation().x - kBoardOrigin.x, + touch->getLocation().y - kBoardOrigin.y); + int selected_tile = + floor(touch_board_location.x / kTileWidthHitBox) + + kTilesX * floor(touch_board_location.y / kTileHeightHitBox); + if (remaining_tiles_.find(selected_tile) == remaining_tiles_.end()) + return true; + + auto sprite = + Sprite::create(kPlayerTokenFileNames[current_player_index_]); + if (sprite == NULL) { + CCLOG("kPlayerTokenFileNames: %s file not found.", + kPlayerTokenFileNames[current_player_index_]); + exit(true); + } + + // Calculates and sets the position of the sprite based on the + // move_tile and the constant screen variables. + const auto x_tile = selected_tile % kTilesX; + const auto y_tile = selected_tile / kTilesY; + sprite->setPosition( + (.5 + x_tile) * kTileWidth + (x_tile)*kBoardLineWidth, + (.5 + y_tile) * kTileHeight + (y_tile)*kBoardLineHeight); + board_sprite_->addChild(sprite); + + // Modify local game state variables to reflect this most recent move + board[selected_tile / kTilesX][selected_tile % kTilesX] = + current_player_index_; + remaining_tiles_.erase(selected_tile); + current_player_index_ = (current_player_index_ + 1) % kNumberOfPlayers; + future_last_move_ = ref_.Child("last_move").SetValue(selected_tile); + future_current_player_index_ = + ref_.Child("current_player_index_").SetValue(current_player_index_); + WaitForCompletion(future_last_move_, "setLastMove"); + WaitForCompletion(future_current_player_index_, "setCurrentPlayerIndex"); + awaiting_opponenet_move_ = true; + waiting_label_->setString("Waiting"); + if (GameOver(board)) { + game_outcome_ = kGameWon; + WaitForCompletion(ref_.Child("game_over").SetValue(true), + "setGameOver"); + } else if (remaining_tiles_.size() == 0) { + // Update game_outcome_ to reflect the user tied. + game_outcome_ = kGameTied; + WaitForCompletion(ref_.Child("game_over").SetValue(true), + "setGameOver"); + } + } + return true; + }; + + Director::getInstance() + ->getEventDispatcher() + ->addEventListenerWithSceneGraphPriority(board_touch_listener, + board_sprite_); + + this->addChild(board_sprite_); +} + +// If the join_game_uid_ is present, initialize game variables, otherwise +// alter the game variables to signify a user joined. Additionally sets the +// player_index_ and total_players based on joining or creating a game. +void TicTacToeLayer::InitializePlayerData() { + if (join_game_uid_.empty()) { + join_game_uid_ = GenerateUid(4); + ref_ = database_->GetReference("game_data").Child(join_game_uid_); + future_create_game_ = ref_.Child("total_players").SetValue(1); + future_current_player_index_ = + ref_.Child("current_player_index_").SetValue(kPlayerOne); + future_game_over_ = ref_.Child("game_over").SetValue(false); + WaitForCompletion(future_game_over_, "setGameOver"); + WaitForCompletion(future_current_player_index_, "setCurrentPlayerIndex"); + WaitForCompletion(future_create_game_, "createGame"); + player_index_ = kPlayerOne; + awaiting_opponenet_move_ = false; + } else { + // Checks whether the join_uid map exists. If it does not then set + // the initialization to failed. + auto future_game_uid = + database_->GetReference("game_data").Child(join_game_uid_).GetValue(); + WaitForCompletion(future_game_uid, "GetGameDataMap"); + auto game_uid_snapshot = future_game_uid.result(); + if (!game_uid_snapshot->value().is_map()) { + initialization_failed_ = true; + } else { + ref_ = database_->GetReference("game_data").Child(join_game_uid_); + auto future_increment_total_users = + ref_.RunTransaction([](firebase::database::MutableData* data) { + auto total_players = data->Child("total_players").value(); + + // Completes the transaction based on the returned mutable data + // value. + if (total_players.is_null()) { + // Must return this if the transaction was unsuccessful. + return TransactionResult::kTransactionResultAbort; + } + int new_total_players = total_players.int64_value() + 1; + if (new_total_players > kNumberOfPlayers) { + // Must return this if the transaction was unsuccessful. + return TransactionResult::kTransactionResultAbort; + } + data->Child("total_players").set_value(new_total_players); + + // Must call this if the transaction was successful. + return TransactionResult::kTransactionResultSuccess; + }); + WaitForCompletion(future_increment_total_users, "JoinGameTransaction"); + + player_index_ = kPlayerTwo; + awaiting_opponenet_move_ = true; + } + } +} +// Creates and sets the listeners as follows. +// ExpectedValueListeners: total_players & game_over. +// SampleValueListener: current_player_index & last_move. +void TicTacToeLayer::InitializeDatabaseListeners() { + // total_player_listener_ and CurrentPlayerIndexListener listener is set up + // to recognise when the desired players have connected & when turns + // alternate. + total_player_listener_ = make_unique(kNumberOfPlayers); + game_over_listener_ = make_unique(true); + + current_player_index_listener_ = make_unique(); + last_move_listener_ = make_unique(); + + ref_.Child("total_players").AddValueListener(total_player_listener_.get()); + ref_.Child("game_over").AddValueListener(game_over_listener_.get()); + ref_.Child("current_player_index_") + .AddValueListener(current_player_index_listener_.get()); + ref_.Child("last_move").AddValueListener(last_move_listener_.get()); +} + +// Counts down the end_game_frames, and updates the user data based on the game +// outcome. +void TicTacToeLayer::EndGame() { + // Removes the game from existence and updates the user's record before + // swap back scenes. + WaitForCompletion( + database_->GetReference("game_data").Child(join_game_uid_).RemoveValue(), + "removeGameUid"); + ref_ = database_->GetReference("users").Child(user_uid_); + + // Updates user record unless the game was disbanded. + if (game_outcome_ != kGameDisbanded) { + auto future_record = + ref_.Child(kGameOutcomeStrings[game_outcome_]).GetValue(); + WaitForCompletion(future_record, "getPreviousOutcomeRecord"); + WaitForCompletion( + ref_.Child(kGameOutcomeStrings[game_outcome_]) + .SetValue(future_record.result()->value().int64_value() + 1), + "setGameOutcomeRecord"); + } + // Pops the scene to return to the previous scene. + Director::getInstance()->popScene(); +} + +void TicTacToeLayer::UpdateBoard() { + int last_move = + last_move_listener_->last_seen_value().AsInt64().int64_value(); + + // Place the players move on the board. + board[last_move / kTilesX][last_move % kTilesX] = current_player_index_; + + // Remove the tile from the tile unordered set. + remaining_tiles_.erase(last_move); + auto sprite = Sprite::create(kPlayerTokenFileNames[current_player_index_]); + if (sprite == NULL) { + CCLOG("kPlayerTokenFileNames: %s file not found.", + kPlayerTokenFileNames[current_player_index_]); + exit(true); + } + + // Calculates and sets the position of the sprite based on the + // move_tile and the constant screen variables. + const auto x_tile = last_move % kTilesX; + const auto y_tile = last_move / kTilesY; + sprite->setPosition((.5 + x_tile) * kTileWidth + (x_tile)*kBoardLineWidth, + (.5 + y_tile) * kTileHeight + (y_tile)*kBoardLineHeight); + board_sprite_->addChild(sprite); + + // Modifies local game state variables to reflect this most recent move. + board[last_move / kTilesX][last_move % kTilesX] = current_player_index_; + remaining_tiles_.erase(last_move); + awaiting_opponenet_move_ = false; + current_player_index_ = player_index_; + if (GameOver(board)) { + // Sets game_outcome_ to reflect the use lost. + game_outcome_ = kGameLost; + WaitForCompletion(ref_.Child("game_over").SetValue(true), "setGameOver"); + } else if (remaining_tiles_.size() == 0) { + // Sets game_outcome_ to reflect the game ended in a tie. + game_outcome_ = kGameTied; + WaitForCompletion(ref_.Child("game_over").SetValue(true), "setGameOver"); + } +} + +void TicTacToeLayer::DisplayGameOutcome() { + displaying_outcome_ = true; + + // Checks to see if the opponenet rage quit. + if (game_outcome_ == kGameDisbanded && + remaining_tiles_.size() != kNumberOfTiles) { + game_outcome_ = kGameWon; + } + + // Creates the delay action. + auto loading_delay = DelayTime::create(/*delay_durration*/ 2.0f); + + // Creates a callable function for EndGame(). + auto RunEndGame = CallFunc::create([this]() { this->EndGame(); }); + + // Runs the sequence that will delay followed by calling EndGame(). + this->runAction(Sequence::create(loading_delay, RunEndGame, NULL)); + + // Creates and displays the game outcome image. + const auto end_game_image = Sprite::create(kGameOutcomeImages[game_outcome_]); + end_game_image->setPosition(Vec2(300, 300)); + this->addChild(end_game_image); +} + +TicTacToeLayer::~TicTacToeLayer() { + // releases our sprite and layer so that it gets deallocated + CC_SAFE_RELEASE_NULL(this->board_sprite_); + CC_SAFE_RELEASE_NULL(this->waiting_label_); +} \ No newline at end of file diff --git a/demos/TicTacToe/game_resources/Classes/tic_tac_toe_layer.h b/demos/TicTacToe/game_resources/Classes/tic_tac_toe_layer.h new file mode 100644 index 00000000..59299686 --- /dev/null +++ b/demos/TicTacToe/game_resources/Classes/tic_tac_toe_layer.h @@ -0,0 +1,116 @@ +// Copyright 2020 Google Inc. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef TICTACTOE_DEMO_CLASSES_TICTACTOELAYER_SCENE_H_ +#define TICTACTOE_DEMO_CLASSES_TICTACTOELAYER_SCENE_H_ + +#include +#include + +#include "cocos/ui/UIButton.h" +#include "cocos2d.h" +#include "firebase/database.h" +#include "firebase/future.h" + +// Tile Constants. +static const int kTilesX = 3; +static const int kTilesY = 3; + +// Firebase listeners. +class SampleValueListener; +class ExpectValueListener; + +class TicTacToeLayer : public cocos2d::Layer { + public: + // Derived from Layer class with input paramters for the game_uid, database + // and user_uid and overrides Layer::update(). + TicTacToeLayer(std::string, firebase::database::Database*, std::string); + ~TicTacToeLayer(); + + private: + // The game loop for this layer which runs every frame once scheduled using + // this->scheduleUpdate(). It constantly checks current_player_index_listener_ + // and game_over_listener so it can take action accordingly. + void update(float) override; + + // Initializes the game board cocos2d componenets and game variables. + void InitializeBoard(); + + // Initializes the player data, based on the join_game_uid. + void InitializePlayerData(); + + // Shows the end game message before returning back to the previous scene. + void EndGame(); + + // Displays the game outcome image then swaps to EndGame(). + void DisplayGameOutcome(); + + // Initializes the value listeners for current_player_, last_move_, game_over_ + // and total_players_. + void InitializeDatabaseListeners(); + + // Updates the board based on the move taken from the game instance in + // the database. + void UpdateBoard(); + + // Tracks whether the board was unable to build. + bool initialization_failed_ = false; + + // Tracks if DisplayGameOutcome been called. + bool displaying_outcome_ = false; + + // Tracks the game outcome. + int game_outcome_; + + // String for the join game code and initialize the database + // reference. + std::string join_game_uid_; + + // User uid to update the user's record after the game is over. + std::string user_uid_; + + // Firebase Realtime Database, the entry point to all database operations. + // + // The database schema has a top level game_uid object which includes + // last_move, total_players and current_player_index_ fields. + firebase::database::Database* database_; + firebase::database::DatabaseReference ref_; + + // Listeners for database values. + std::unique_ptr current_player_index_listener_; + std::unique_ptr last_move_listener_; + std::unique_ptr total_player_listener_; + std::unique_ptr game_over_listener_; + + // Label, button and a sprite. + cocos2d::Sprite* board_sprite_; + cocos2d::ui::Button* leave_button_; + cocos2d::Label* waiting_label_; + + // Firebase futures for last_move and current_player_index_. + firebase::Future future_last_move_; + firebase::Future future_current_player_index_; + firebase::Future future_game_over_; + firebase::Future future_create_game_; + + int current_player_index_; + int player_index_; + int board[kTilesX][kTilesY]; + + bool awaiting_opponenet_move_; + + // Unordered set of remaining tiles available for player moves. + std::unordered_set remaining_tiles_; +}; +#endif // TICTACTOE_DEMO_CLASSES_TICTACTOELAYER_SCENE_H_ \ No newline at end of file diff --git a/demos/TicTacToe/game_resources/Classes/tic_tac_toe_scene.cc b/demos/TicTacToe/game_resources/Classes/tic_tac_toe_scene.cc new file mode 100644 index 00000000..cb8b2723 --- /dev/null +++ b/demos/TicTacToe/game_resources/Classes/tic_tac_toe_scene.cc @@ -0,0 +1,40 @@ +// Copyright 2020 Google Inc. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "tic_tac_toe_scene.h" + +#include + +#include "cocos2d.h" +#include "firebase/database.h" +#include "tic_tac_toe_layer.h" + +using cocos2d::Scene; + +Scene* TicTacToe::createScene(const std::string& game_uid, + firebase::database::Database* main_menu_database, + const std::string& main_menu_user_uid) { + // Sets the join_game_uid to the passed in game_uid. + // Builds a simple scene that uses the bottom left cordinate point as (0,0) + // and can have sprites, labels and layers added onto it. + Scene* scene = Scene::create(); + + // Builds a layer to be placed onto the scene which has access to TouchEvents. + // This TicTacToe layer created is owned by the scene. + auto tic_tac_toe_layer = + new TicTacToeLayer(game_uid, main_menu_database, main_menu_user_uid); + scene->addChild(tic_tac_toe_layer); + + return scene; +} diff --git a/demos/TicTacToe/game_resources/Classes/tic_tac_toe_scene.h b/demos/TicTacToe/game_resources/Classes/tic_tac_toe_scene.h new file mode 100644 index 00000000..e0b31b5b --- /dev/null +++ b/demos/TicTacToe/game_resources/Classes/tic_tac_toe_scene.h @@ -0,0 +1,33 @@ +// Copyright 2020 Google Inc. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef TICTACTOE_DEMO_CLASSES_TICTACTOE_SCENE_H_ +#define TICTACTOE_DEMO_CLASSES_TICTACTOE_SCENE_H_ + +#include "cocos2d.h" +#include "firebase/database.h" + +class TicTacToe : public cocos2d::Layer { + public: + // Builds a simple scene that uses the bottom left cordinate point as (0,0) + // and can have sprites, labels and nodes added onto it. + static cocos2d::Scene* createScene(const std::string&, + firebase::database::Database*, + const std::string&); + + private: + // Defines a create type for a specific type, in this case a Layer. + CREATE_FUNC(TicTacToe); +}; +#endif // TICTACTOE_DEMO_CLASSES_TICTACTOE_SCENE_H_ \ No newline at end of file diff --git a/demos/TicTacToe/game_resources/Classes/util.cc b/demos/TicTacToe/game_resources/Classes/util.cc new file mode 100644 index 00000000..ac93cdc0 --- /dev/null +++ b/demos/TicTacToe/game_resources/Classes/util.cc @@ -0,0 +1,70 @@ +// Copyright 2020 Google Inc. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "util.h" + +#include +#include + +#include + +// Logs the message passed in to the console. +void LogMessage(const char* format, ...) { + va_list list; + va_start(list, format); + vprintf(format, list); + va_end(list); + printf("\n"); + fflush(stdout); +} + +// Based on operating system, waits for the specified amount of miliseconds. +void ProcessEvents(int msec) { +#ifdef _WIN32 + Sleep(msec); +#else + usleep(msec * 1000); +#endif // _WIN32 +} + +// Wait for a Future to be completed. If the Future returns an error, it will +// be logged. +void WaitForCompletion(const firebase::FutureBase& future, const char* name) { + while (future.status() == firebase::kFutureStatusPending) { + ProcessEvents(100); + } + if (future.status() != firebase::kFutureStatusComplete) { + LogMessage("ERROR: %s returned an invalid result.", name); + } else if (future.error() != 0) { + LogMessage("ERROR: %s returned error %d: %s", name, future.error(), + future.error_message()); + } +} + +// Generates a random uid of a specified length. +std::string GenerateUid(std::size_t length) { + const std::string kCharacters = "123456789ABCDEFGHJKMNPQRSTUVXYZ"; + + std::random_device random_device; + std::mt19937 generator(random_device()); + std::uniform_int_distribution<> distribution(0, kCharacters.size() - 1); + + std::string generate_uid; + + for (std::size_t i = 0; i < length; ++i) { + generate_uid += kCharacters[distribution(generator)]; + } + + return generate_uid; +} diff --git a/demos/TicTacToe/game_resources/Classes/util.h b/demos/TicTacToe/game_resources/Classes/util.h new file mode 100644 index 00000000..c43906ad --- /dev/null +++ b/demos/TicTacToe/game_resources/Classes/util.h @@ -0,0 +1,45 @@ +// Copyright 2020 Google Inc. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +#ifndef TICTACTOE_DEMO_CLASSES_UTIL_H_ +#define TICTACTOE_DEMO_CLASSES_UTIL_H_ + +#include + +#include "firebase/future.h" + +// inputs: const char* as the message that will be displayed. +// +// Logs the message to the user through the console. +void LogMessage(const char*, ...); + +// inputs: integer for number of miliseconds. +// +// Acts as a blocking statement to wait for the specified time. +void ProcessEvents(int); + +// inputs: FutureBase waiting to be completed and message describing the future +// action. +// +// Spins on ProcessEvents() while the FutureBase's status is pending, which it +// will then break out and LogMessage() notifying the FutureBase's status is +// completed. +void WaitForCompletion(const firebase::FutureBase&, const char*); + +// inputs: size_t integer specifying the length of the uid. +// +// Generates a unique string based on the length passed in, grabbing the allowed +// characters from the character string defined inside the function. +std::string GenerateUid(std::size_t); + +#endif // TICTACTOE_DEMO_CLASSES_UTIL_H_ diff --git a/demos/TicTacToe/game_resources/Resources/auth_panel.png b/demos/TicTacToe/game_resources/Resources/auth_panel.png new file mode 100644 index 00000000..1954a223 Binary files /dev/null and b/demos/TicTacToe/game_resources/Resources/auth_panel.png differ diff --git a/demos/TicTacToe/game_resources/Resources/back.png b/demos/TicTacToe/game_resources/Resources/back.png new file mode 100644 index 00000000..acf6fd5d Binary files /dev/null and b/demos/TicTacToe/game_resources/Resources/back.png differ diff --git a/demos/TicTacToe/game_resources/Resources/back_dark.png b/demos/TicTacToe/game_resources/Resources/back_dark.png new file mode 100644 index 00000000..acf6fd5d Binary files /dev/null and b/demos/TicTacToe/game_resources/Resources/back_dark.png differ diff --git a/demos/TicTacToe/game_resources/Resources/background.png b/demos/TicTacToe/game_resources/Resources/background.png new file mode 100644 index 00000000..75118291 Binary files /dev/null and b/demos/TicTacToe/game_resources/Resources/background.png differ diff --git a/demos/TicTacToe/game_resources/Resources/create_game.png b/demos/TicTacToe/game_resources/Resources/create_game.png new file mode 100644 index 00000000..0071b543 Binary files /dev/null and b/demos/TicTacToe/game_resources/Resources/create_game.png differ diff --git a/demos/TicTacToe/game_resources/Resources/create_game_dark.png b/demos/TicTacToe/game_resources/Resources/create_game_dark.png new file mode 100644 index 00000000..efc7ba8a Binary files /dev/null and b/demos/TicTacToe/game_resources/Resources/create_game_dark.png differ diff --git a/demos/TicTacToe/game_resources/Resources/enter_code.png b/demos/TicTacToe/game_resources/Resources/enter_code.png new file mode 100644 index 00000000..073ff72f Binary files /dev/null and b/demos/TicTacToe/game_resources/Resources/enter_code.png differ diff --git a/demos/TicTacToe/game_resources/Resources/enter_code_dark.png b/demos/TicTacToe/game_resources/Resources/enter_code_dark.png new file mode 100644 index 00000000..df71e562 Binary files /dev/null and b/demos/TicTacToe/game_resources/Resources/enter_code_dark.png differ diff --git a/demos/TicTacToe/game_resources/Resources/fonts/GoogleSans-Regular.ttf b/demos/TicTacToe/game_resources/Resources/fonts/GoogleSans-Regular.ttf new file mode 100644 index 00000000..ab605f9e Binary files /dev/null and b/demos/TicTacToe/game_resources/Resources/fonts/GoogleSans-Regular.ttf differ diff --git a/demos/TicTacToe/game_resources/Resources/game_menu_panel.png b/demos/TicTacToe/game_resources/Resources/game_menu_panel.png new file mode 100644 index 00000000..f4bffb05 Binary files /dev/null and b/demos/TicTacToe/game_resources/Resources/game_menu_panel.png differ diff --git a/demos/TicTacToe/game_resources/Resources/join_game.png b/demos/TicTacToe/game_resources/Resources/join_game.png new file mode 100644 index 00000000..2fea401c Binary files /dev/null and b/demos/TicTacToe/game_resources/Resources/join_game.png differ diff --git a/demos/TicTacToe/game_resources/Resources/join_game_dark.png b/demos/TicTacToe/game_resources/Resources/join_game_dark.png new file mode 100644 index 00000000..783a6ae4 Binary files /dev/null and b/demos/TicTacToe/game_resources/Resources/join_game_dark.png differ diff --git a/demos/TicTacToe/game_resources/Resources/leave.png b/demos/TicTacToe/game_resources/Resources/leave.png new file mode 100644 index 00000000..ed4c773f Binary files /dev/null and b/demos/TicTacToe/game_resources/Resources/leave.png differ diff --git a/demos/TicTacToe/game_resources/Resources/leave_anon.png b/demos/TicTacToe/game_resources/Resources/leave_anon.png new file mode 100644 index 00000000..8db37a3d Binary files /dev/null and b/demos/TicTacToe/game_resources/Resources/leave_anon.png differ diff --git a/demos/TicTacToe/game_resources/Resources/leave_anon_dark.png b/demos/TicTacToe/game_resources/Resources/leave_anon_dark.png new file mode 100644 index 00000000..f52fb551 Binary files /dev/null and b/demos/TicTacToe/game_resources/Resources/leave_anon_dark.png differ diff --git a/demos/TicTacToe/game_resources/Resources/leave_button.png b/demos/TicTacToe/game_resources/Resources/leave_button.png new file mode 100644 index 00000000..ed4c773f Binary files /dev/null and b/demos/TicTacToe/game_resources/Resources/leave_button.png differ diff --git a/demos/TicTacToe/game_resources/Resources/leave_dark.png b/demos/TicTacToe/game_resources/Resources/leave_dark.png new file mode 100644 index 00000000..741c3bdc Binary files /dev/null and b/demos/TicTacToe/game_resources/Resources/leave_dark.png differ diff --git a/demos/TicTacToe/game_resources/Resources/loading_background.png b/demos/TicTacToe/game_resources/Resources/loading_background.png new file mode 100644 index 00000000..71bfbb2e Binary files /dev/null and b/demos/TicTacToe/game_resources/Resources/loading_background.png differ diff --git a/demos/TicTacToe/game_resources/Resources/login.png b/demos/TicTacToe/game_resources/Resources/login.png new file mode 100644 index 00000000..4e94cac6 Binary files /dev/null and b/demos/TicTacToe/game_resources/Resources/login.png differ diff --git a/demos/TicTacToe/game_resources/Resources/login_dark.png b/demos/TicTacToe/game_resources/Resources/login_dark.png new file mode 100644 index 00000000..ddb4e12b Binary files /dev/null and b/demos/TicTacToe/game_resources/Resources/login_dark.png differ diff --git a/demos/TicTacToe/game_resources/Resources/login_panel.png b/demos/TicTacToe/game_resources/Resources/login_panel.png new file mode 100644 index 00000000..0313e2c4 Binary files /dev/null and b/demos/TicTacToe/game_resources/Resources/login_panel.png differ diff --git a/demos/TicTacToe/game_resources/Resources/logout.png b/demos/TicTacToe/game_resources/Resources/logout.png new file mode 100644 index 00000000..76cc0f7d Binary files /dev/null and b/demos/TicTacToe/game_resources/Resources/logout.png differ diff --git a/demos/TicTacToe/game_resources/Resources/logout_dark.png b/demos/TicTacToe/game_resources/Resources/logout_dark.png new file mode 100644 index 00000000..313a8d4a Binary files /dev/null and b/demos/TicTacToe/game_resources/Resources/logout_dark.png differ diff --git a/demos/TicTacToe/game_resources/Resources/outcome_disbanded.png b/demos/TicTacToe/game_resources/Resources/outcome_disbanded.png new file mode 100644 index 00000000..15cd9814 Binary files /dev/null and b/demos/TicTacToe/game_resources/Resources/outcome_disbanded.png differ diff --git a/demos/TicTacToe/game_resources/Resources/outcome_lost.png b/demos/TicTacToe/game_resources/Resources/outcome_lost.png new file mode 100644 index 00000000..d21500fb Binary files /dev/null and b/demos/TicTacToe/game_resources/Resources/outcome_lost.png differ diff --git a/demos/TicTacToe/game_resources/Resources/outcome_tied.png b/demos/TicTacToe/game_resources/Resources/outcome_tied.png new file mode 100644 index 00000000..f09f0cbc Binary files /dev/null and b/demos/TicTacToe/game_resources/Resources/outcome_tied.png differ diff --git a/demos/TicTacToe/game_resources/Resources/outcome_won.png b/demos/TicTacToe/game_resources/Resources/outcome_won.png new file mode 100644 index 00000000..e4007913 Binary files /dev/null and b/demos/TicTacToe/game_resources/Resources/outcome_won.png differ diff --git a/demos/TicTacToe/game_resources/Resources/res/.gitkeep b/demos/TicTacToe/game_resources/Resources/res/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/demos/TicTacToe/game_resources/Resources/sign_up.png b/demos/TicTacToe/game_resources/Resources/sign_up.png new file mode 100644 index 00000000..91e399fc Binary files /dev/null and b/demos/TicTacToe/game_resources/Resources/sign_up.png differ diff --git a/demos/TicTacToe/game_resources/Resources/sign_up_dark.png b/demos/TicTacToe/game_resources/Resources/sign_up_dark.png new file mode 100644 index 00000000..a30d96f4 Binary files /dev/null and b/demos/TicTacToe/game_resources/Resources/sign_up_dark.png differ diff --git a/demos/TicTacToe/game_resources/Resources/sign_up_panel.png b/demos/TicTacToe/game_resources/Resources/sign_up_panel.png new file mode 100644 index 00000000..115999b6 Binary files /dev/null and b/demos/TicTacToe/game_resources/Resources/sign_up_panel.png differ diff --git a/demos/TicTacToe/game_resources/Resources/skip.png b/demos/TicTacToe/game_resources/Resources/skip.png new file mode 100644 index 00000000..0ff9cc8d Binary files /dev/null and b/demos/TicTacToe/game_resources/Resources/skip.png differ diff --git a/demos/TicTacToe/game_resources/Resources/skip_dark.png b/demos/TicTacToe/game_resources/Resources/skip_dark.png new file mode 100644 index 00000000..c09ecd21 Binary files /dev/null and b/demos/TicTacToe/game_resources/Resources/skip_dark.png differ diff --git a/demos/TicTacToe/game_resources/Resources/text_field_grey.png b/demos/TicTacToe/game_resources/Resources/text_field_grey.png new file mode 100644 index 00000000..6e590d34 Binary files /dev/null and b/demos/TicTacToe/game_resources/Resources/text_field_grey.png differ diff --git a/demos/TicTacToe/game_resources/Resources/text_field_white.png b/demos/TicTacToe/game_resources/Resources/text_field_white.png new file mode 100644 index 00000000..1141f6a8 Binary files /dev/null and b/demos/TicTacToe/game_resources/Resources/text_field_white.png differ diff --git a/demos/TicTacToe/game_resources/Resources/tic_tac_toe_board.png b/demos/TicTacToe/game_resources/Resources/tic_tac_toe_board.png new file mode 100644 index 00000000..84b43378 Binary files /dev/null and b/demos/TicTacToe/game_resources/Resources/tic_tac_toe_board.png differ diff --git a/demos/TicTacToe/game_resources/Resources/tic_tac_toe_o.png b/demos/TicTacToe/game_resources/Resources/tic_tac_toe_o.png new file mode 100644 index 00000000..e155fba0 Binary files /dev/null and b/demos/TicTacToe/game_resources/Resources/tic_tac_toe_o.png differ diff --git a/demos/TicTacToe/game_resources/Resources/tic_tac_toe_x.png b/demos/TicTacToe/game_resources/Resources/tic_tac_toe_x.png new file mode 100644 index 00000000..ada9ab27 Binary files /dev/null and b/demos/TicTacToe/game_resources/Resources/tic_tac_toe_x.png differ diff --git a/demos/TicTacToe/game_resources/Resources/user_record_panel.png b/demos/TicTacToe/game_resources/Resources/user_record_panel.png new file mode 100644 index 00000000..9d171fb5 Binary files /dev/null and b/demos/TicTacToe/game_resources/Resources/user_record_panel.png differ diff --git a/demos/TicTacToe/game_resources/build/bin/tic_tac_toe_demo/Debug/.gitkeep b/demos/TicTacToe/game_resources/build/bin/tic_tac_toe_demo/Debug/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/demos/TicTacToe/game_resources/proj.win32/res/game.ico b/demos/TicTacToe/game_resources/proj.win32/res/game.ico new file mode 100644 index 00000000..b7494836 Binary files /dev/null and b/demos/TicTacToe/game_resources/proj.win32/res/game.ico differ diff --git a/demos/TicTacToe/google_services/.gitkeep b/demos/TicTacToe/google_services/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/demos/TicTacToe/readme.md b/demos/TicTacToe/readme.md new file mode 100644 index 00000000..8c34af72 --- /dev/null +++ b/demos/TicTacToe/readme.md @@ -0,0 +1,127 @@ +Firebase Cocos2d-x TicTacToe Sample +========================== + +Desktop cocos2d-x sample using Firebase Real-Time Database and Firebase Authentication. + +Introduction +------------ + +- [Read more about Firebase](https://firebase.google.com/docs) + +Prerequisites +------------- +- [Cmake](https://cmake.org/download/) (Minimum 3.6) + +- [Python 2.7](https://www.python.org/download/releases/2.7/) + +- The latest version [of Cocos2d-x](https://cocos2d-x.org/download) (cocos environment variable must be added to PATH) + +- [Firebase CPP SDK](https://cocos2d-x.org/download) (Must be add to path under FIREBASE_CPP_SDK_DIR) + +- Windows: ([Visual Studio 2019](https://visualstudio.microsoft.com/downloads/)) + +Getting Started +--------------- + +- Clone the firebase cpp quickstart GitHub repo. + ``` + git clone https://github.com/firebase/quickstart-cpp.git + ``` + +### Windows +- Follow the steps in + [Set up your app in Firebase console](https://firebase.google.com/docs/cpp/setup?platform=android#desktop-workflow). + +- Enable Anonymous Auth & Email/Password sign-in: + a. In the [Firebase console](https://console.firebase.google.com/), open the Auth section. + b. On the Sign in method tab, enable the Anonymous Auth & Email/password sign-in method and click Save. + +- Place the google-services.json you just acquired in the demos/TicTacToe/google_services/ directory. + +- Navigate to the directory that you just cloned, step into demos/TicTacToe/ and run the setup script. + ``` + python setup_demo.py + ``` + +- To debug with Visual Studio 2019, copy the google-services.json file into the tic_tac_toe_demo/build/ directory. Then open the .sln in that same directory in Visual Studio. + +### How To Play +--------------- + +- After you have successfully run the setup_demo.py script, you can then launch the game by running the run_demo.py script. In order to test the multiplayer functionality, open up another terminal and launch a second instant of the game by running the run_demo.py script again. + ``` + python run_demo.py + ``` + +- Using the sign-up button or login button will link your record (Wins-Losses-Ties) to your account. The skip login button will not save your record between sessions. + +- From the main menu screen, you can either create a game or join using a game code (displayed in the top-left corner of the game board). If you are running this demo yourself, have on instance create the game and join with your second game instance. + +- After finishing the game, you will notice the record will update on the main menu to reflect the result of your most recent game. + + + +## Future Improvements +--------------- + +### Accessability & Build Process + +#### 1. Build on MacOS. + By making slight modifications to the setup_demo.py script, making cmake generator an argument, the application should build and run on MacOS. + + +#### 2. Build on linux. + By making slight modifications to the setup_demo.py script, making cmake generator an argument, the application should build and run on linux. + + +#### 3. Build on Android + +#### 4. Build on iOS + +#### 5. Cmake optimizations. + - Use FetchContent to grab the latest Firebase SDK. + - Attempt to get around Python 2.7 dependency, this may already work as the scripts use cmake. + - Attempt to use FetchContent to grab Cocos2d-x library and grab the files currently being produced by the 'cocos new' command. + + +### Features + +#### 1. Add a replay system. + This will require changing the game_data documents to keep track of the entire game, oppose to the last_move as it currently does. + + +#### 2. Add animations and sounds on actions/scene changes. + + +#### 3. Text field improvements. (Potentially swap to EditBox as this may be better supported) + - If the Tab key is pressed inside a text field, it should attempt to move to then next text field. + - When a player pressed inside a text field will existing text, it should place the cursor in the selected location. + + +#### 4. Add Additional Authentication providers. + +### Bugs + +#### 1. Close application callback. + If the player closes the application, this should trigger a callback to update relevant Firebase feature data. + +#### Code Health + +### 1. Remove all blocking statements (WaitForCompletion()). + Removes the WaitForCompletion() function. Instead, rely on the state management to handle future requests. + +Support +------- + +https://firebase.google.com/support/ + +License +------- + +Copyright 2020 Google, Inc. + +Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.. \ No newline at end of file diff --git a/demos/TicTacToe/rebuild_demo.py b/demos/TicTacToe/rebuild_demo.py new file mode 100644 index 00000000..f95e77aa --- /dev/null +++ b/demos/TicTacToe/rebuild_demo.py @@ -0,0 +1,72 @@ +#!/usr/bin/python +# coding=utf-8 + +# Copyright 2020 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging +import os +import subprocess +import sys + + +def logger_setup(): + # The root logger of the hierarchy. + logger = logging.getLogger() + logger.setLevel(logging.DEBUG) + + # Add a StreamHandler to log to stdout. + stream_handler = logging.StreamHandler(sys.stdout) + stream_handler.setLevel(logging.DEBUG) + formatter = logging.Formatter( + "%(asctime)s - %(name)s - %(levelname)s - %(message)s") + stream_handler.setFormatter(formatter) + logger.addHandler(stream_handler) + + return logger + + +def log_run(dir, logger, cmd): + # Logs the command. + logger.info(cmd) + # Runs the command. + subprocess.call(cmd, cwd=dir, shell=True) + + +def main(): + """The main function.""" + # The run_demo.py script directory. + ROOT_DIRECTORY = os.path.dirname(os.path.abspath(__file__)) + + # Sets up the logging format and handler. + logger = logger_setup() + + # Directory and file paths. + game_name = "tic_tac_toe_demo" + build_dir = os.path.join(ROOT_DIRECTORY, game_name, "build") + + # Checks whether the build directory was created. + if os.path.isdir(build_dir): + logger.info("Building the demo...") + # Builds the tic_tac_toe_demo executable. + log_run(build_dir, logger, "cmake --build .") + else: + logger.error( + "Build directory expected at {} does not exist.".format(build_dir)) + exit() + + +# Check to see if this script is being called directly. +if __name__ == "__main__": + exit(main()) diff --git a/demos/TicTacToe/run_demo.py b/demos/TicTacToe/run_demo.py new file mode 100644 index 00000000..83418644 --- /dev/null +++ b/demos/TicTacToe/run_demo.py @@ -0,0 +1,87 @@ +#!/usr/bin/python +# coding=utf-8 + +# Copyright 2020 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging +import os +import subprocess +import sys + + +def logger_setup(): + # The root logger of the hierarchy. + logger = logging.getLogger() + logger.setLevel(logging.DEBUG) + + # Add a StreamHandler to log to stdout. + stream_handler = logging.StreamHandler(sys.stdout) + stream_handler.setLevel(logging.DEBUG) + formatter = logging.Formatter( + "%(asctime)s - %(name)s - %(levelname)s - %(message)s") + stream_handler.setFormatter(formatter) + logger.addHandler(stream_handler) + + return logger + + +def log_run(dir, logger, cmd): + # Logs the command. + logger.info(cmd) + # Runs the command. + subprocess.call(cmd, cwd=dir, shell=True) + + +def main(): + """The main function.""" + # The run_demo.py script directory. + ROOT_DIRECTORY = os.path.dirname(os.path.abspath(__file__)) + + # Sets up the logging format and handler. + logger = logger_setup() + + # Directory paths. + game_name = "tic_tac_toe_demo" + executable_dir = os.path.join(ROOT_DIRECTORY, game_name, "build", "bin", + game_name, "Debug") + + # Checks whether the demo was set up properly. + if not os.path.isdir(executable_dir): + logger.error( + "Demo setup incomplete. Did you run the setup script first?") + exit() + + # Checks whether the google-services.json exists in the debug directory. + if not os.path.isfile(os.path.join(executable_dir, + "google-services.json")): + logger.error( + "google-services.json file is missing in {} directory.".format( + executable_dir)) + exit() + + # Checks whether the executable exists in the debug directory. + if os.path.isfile(os.path.join(executable_dir, + "{}.exe".format(game_name))): + logger.info("Lanching the demo...") + # Runs the tic-tac-toe executable. + log_run(executable_dir, logger, '{}.exe'.format(game_name)) + else: + logger.error("Game executable does not exist.") + exit() + + +# Check to see if this script is being called directly. +if __name__ == "__main__": + exit(main()) diff --git a/demos/TicTacToe/setup_demo.py b/demos/TicTacToe/setup_demo.py new file mode 100644 index 00000000..4dd40fef --- /dev/null +++ b/demos/TicTacToe/setup_demo.py @@ -0,0 +1,121 @@ +#!/usr/bin/python +# coding=utf-8 + +# Copyright 2020 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import fileinput +import logging +import os +import subprocess +import sys + + +def logger_setup(): + # The root logger of the hierarchy. + logger = logging.getLogger() + logger.setLevel(logging.DEBUG) + + # Add a StreamHandler to log to stdout. + stream_handler = logging.StreamHandler(sys.stdout) + stream_handler.setLevel(logging.DEBUG) + formatter = logging.Formatter( + "%(asctime)s - %(name)s - %(levelname)s - %(message)s") + stream_handler.setFormatter(formatter) + logger.addHandler(stream_handler) + + return logger + + +def log_run(dir, logger, cmd): + # Logs the command. + logger.info(cmd) + # Runs the command. + subprocess.call(cmd, cwd=dir, shell=True) + + +def modify_proj_file(dir): + f = fileinput.FileInput(files=[os.path.join(dir, "main.cpp")], + inplace=True) + for line in f: + print line.replace("AppDelegate.h", "app_delegate.h"), + + +def main(): + """The main function.""" + # The setup_demo.py script directory. + ROOT_DIRECTORY = os.path.dirname(os.path.abspath(__file__)) + + # Sets up the logging format and handler. + logger = logger_setup() + + # Directory and file paths. + game_name = "tic_tac_toe_demo" + game_resources_dir = os.path.join(ROOT_DIRECTORY, "game_resources") + game_files_dir = os.path.join(ROOT_DIRECTORY, game_name) + google_services_path = os.path.join(ROOT_DIRECTORY, "google_services", + "google-services.json") + windows_proj_dir = os.path.join(game_files_dir, "proj.win32") + mac_proj_dir = os.path.join(game_files_dir, "proj.ios_mac", "mac") + linux_proj_dir = os.path.join(game_files_dir, "proj.linux") + build_dir = os.path.join(game_files_dir, "build") + executable_dir = os.path.join(build_dir, "bin", game_name, "Debug") + + # Checks whether the google-services.json exists in the debug directory. + if not os.path.isfile(google_services_path): + # Runs the tic-tac-toe executable. + logger.error("google_services/google-services.json is missing.") + exit() + + # Creating the cocos2d-x project. + log_run(ROOT_DIRECTORY, logger, + "cocos new {0} -p com.DemoApp.{0} -l cpp -d .".format(game_name)) + + # Removing the default cocos2d-x project files. + log_run( + ROOT_DIRECTORY, logger, + "rm -r {0}/Classes {0}/Resources {0}/CMakeLists.txt".format( + game_files_dir)) + + # Copies the google-services.json file into the correct directory to run the executable. + log_run(ROOT_DIRECTORY, logger, "cp {} {}".format(google_services_path, + executable_dir)) + + # Copies the tic-tac-toe game files into the cocos2d-x project files. + log_run(ROOT_DIRECTORY, logger, + "cp {} {} -TRv".format(game_resources_dir, game_files_dir)) + + # Changes the windows project main.cpp to include the new app_delegate header. + modify_proj_file(windows_proj_dir) + + # Runs cmake with the Visual Studio 2019 as the generator and windows as the target. + log_run(build_dir, logger, 'cmake .. -G "Visual Studio 16 2019" -A Win32') + + # Builds the tic_tac_toe_demo executable. + log_run(build_dir, logger, "cmake --build .") + + # Copies the google-services.json file into the build directory for debugging purposes. + log_run(ROOT_DIRECTORY, logger, "cp {} {}".format(google_services_path, + build_dir)) + + # Checks whether the google-services.json exists in the debug directory. + if os.path.isfile(os.path.join(executable_dir, game_name + ".exe")): + logger.info("Demo setup succeeded.") + else: + logger.error("Demo setup failed.") + + +# Check to see if this script is being called directly. +if __name__ == "__main__": + exit(main())