Created
November 11, 2019 09:43
-
-
Save yoavst/ace6847b3f8a46a5748874445c7e8230 to your computer and use it in GitHub Desktop.
Aluf Hamikraot bot
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.yoavst.testing.project | |
import android.content.Intent | |
import android.widget.ImageView | |
import androidx.test.ext.junit.runners.AndroidJUnit4 | |
import androidx.test.platform.app.InstrumentationRegistry | |
import androidx.test.uiautomator.* | |
import org.hamcrest.MatcherAssert | |
import org.hamcrest.Matchers | |
import org.junit.Before | |
import org.junit.Test | |
import org.junit.runner.RunWith | |
@RunWith(AndroidJUnit4::class) | |
class AlufWinner { | |
private lateinit var device: UiDevice | |
private lateinit var game: GameController | |
@Before | |
fun launchApp() { | |
// Initialize UiDevice instance | |
device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) | |
game = GameController(device) | |
openApp() | |
} | |
private fun openApp() { | |
if (SHOULD_OPEN_APP) { | |
// Start from the home screen | |
device.pressHome() | |
// Wait for launcher | |
val launcherPackage = device.launcherPackageName | |
MatcherAssert.assertThat(launcherPackage, Matchers.notNullValue()) | |
device.wait(Until.hasObject(By.pkg(launcherPackage).depth(0)), LAUNCH_TIMEOUT) | |
// Launch the app | |
val context = InstrumentationRegistry.getInstrumentation().targetContext | |
val intent = context.packageManager.getLaunchIntentForPackage(PACKAGE_NAME) | |
?: throw IllegalStateException("Application is not installed") | |
// Clear out any previous instances | |
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK) | |
context.startActivity(intent) | |
// Wait for the app to appear | |
device.wait(Until.hasObject(By.pkg(PACKAGE_NAME).depth(0)), LAUNCH_TIMEOUT * 2) | |
// Open game activity | |
val gamesModeBtn = | |
device.findObject(UiSelector().className(ImageView::class.java).instance(2)) | |
gamesModeBtn.clickAndWaitForNewWindow() | |
} | |
} | |
private fun startFirstGame() { | |
if (SHOULD_OPEN_APP) { | |
val playBtn = device.findObject(UiSelector().text("שחק")) | |
playBtn.clickAndWaitForNewWindow() | |
} | |
} | |
@Test | |
fun runBot() { | |
startFirstGame() | |
while (true) { | |
trying(game::tryPlayRound) | |
game.tryPlayAgain() | |
game.tryCloseAd() | |
} | |
} | |
} | |
private const val PACKAGE_NAME = "org.apache.cordova.Mikraot" | |
private const val LAUNCH_TIMEOUT = 5000L | |
private const val SHOULD_OPEN_APP = true | |
private inline fun trying(func: () -> Unit) { | |
try { | |
func() | |
} catch (e: UiObjectNotFoundException) { | |
e.printStackTrace() | |
} catch (e: StaleObjectException) { | |
e.printStackTrace() | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.yoavst.testing.project | |
import android.util.Log | |
import com.github.salomonbrys.kotson.fromJson | |
import com.google.gson.Gson | |
private val strangeQuestions = setOf( | |
"איזה מהבאים חיבר הרמב\"ם?", | |
"איזו מצווה נהוגה בפורים?", | |
"מה מהבאים עמד בבסיס האידיאולוגיה הנאצית?", | |
"מדוע הוקמה \"ועדת פיל\"?", | |
"מהי ''התיוונות''?" | |
) | |
class Competitor(private val cache: MutableMap<Question, String> = mutableMapOf()) { | |
/** | |
* Tries to answer a question. | |
* If it has already seen the question, provides the right answer | |
*/ | |
operator fun get(question: Question): Int? { | |
if (question.question in strangeQuestions) { | |
return when (question.question) { | |
"מה מהבאים עמד בבסיס האידיאולוגיה הנאצית?" -> if ("הצורך במרחב מחייה" in question.answers && "השמדת יהודים" !in question.answers && "טיהור עולמי" !in question.answers) { | |
question.answers.indexOf("הצורך במרחב מחייה") | |
} else { | |
question.answers.indexOf("כל התשובות נכונות") | |
} | |
"מדוע הוקמה \"ועדת פיל\"?" -> if ("כדי להציע פתרון לסכסוך היהודי-ערבי" in question.answers) { | |
question.answers.indexOf("כדי להציע פתרון לסכסוך היהודי-ערבי") | |
} else { | |
question.answers.indexOf("בעקבות מאורעות תרצ\"ו") | |
} | |
"איזו מצווה נהוגה בפורים?" -> if ("קריאת המגילה" in question.answers) { | |
question.answers.indexOf("קריאת המגילה") | |
} else { | |
question.answers.indexOf("מתנות לאביונים") | |
} | |
"איזה מהבאים חיבר הרמב\"ם?" -> if ("משנה תורה" in question.answers) { | |
question.answers.indexOf("משנה תורה") | |
} else { | |
question.answers.indexOf("מורה נבוכים") | |
} | |
"מהי ''התיוונות''?" -> question.answers.indexOf("קבלת התרבות היוונית וישומה") | |
else -> -1 | |
} | |
} | |
if (question in cache) { | |
val index = question.answers.indexOf(cache[question]) | |
if (index >= 0) | |
return index | |
Log.w("aluf-bot", "Conflict!!! $question ${cache[question]}") | |
return -1 | |
} | |
return null | |
} | |
/** | |
* Updates a question with the given answer | |
*/ | |
operator fun set(question: Question, answer: Int) { | |
if (question !in cache) { | |
Log.i("aluf-bot", "Database size is ${cache.size}") | |
} | |
cache[question] = question.answers[answer] | |
} | |
fun export(): String { | |
return Gson().toJson(cache.toList()) | |
} | |
companion object { | |
fun of(dump: String) = | |
Competitor(Gson().fromJson<List<Pair<Question, String>>>(dump).associate { it }.toMutableMap()) | |
} | |
} | |
data class Question(val question: String, val answers: List<String>) { | |
override fun hashCode(): Int = question.hashCode() | |
override fun equals(other: Any?) = other is Question && question == other.question | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.yoavst.testing.project | |
import android.graphics.BitmapFactory | |
import android.graphics.Color | |
import android.util.Log | |
import android.widget.Button | |
import android.widget.ImageView | |
import androidx.core.graphics.get | |
import androidx.test.uiautomator.By | |
import androidx.test.uiautomator.UiDevice | |
import androidx.test.uiautomator.UiObject | |
import androidx.test.uiautomator.UiSelector | |
import java.io.File | |
import java.lang.Thread.sleep | |
class GameController( | |
private val device: UiDevice, | |
private val competitor: Competitor = Competitor.of(DATA_FILE.readText()) | |
) { | |
private var answeredQuestions = 0 | |
private val questionsSeen = mutableSetOf<String>() | |
fun tryPlayRound() { | |
if (!device.hasObject(By.textContains("שניות"))) | |
return | |
val buttonOffset = if (device.hasObject(By.textContains("ספונסר"))) 1 else 0 | |
// get question | |
val questionText = | |
device.findObjects(By.clickable(true).clazz(ImageView::class.java)) | |
.firstOrNull { it.visibleBounds.left == LEFT_OF_ANSWER }?.parent?.text ?: return | |
// get answers | |
val answerButtons = (buttonOffset..(ANSWERS_COUNT - 1 + buttonOffset)).map { | |
UiSelector().clickable(true).className(ImageView::class.java).instance(it) | |
}.map(device::findObject) | |
val answers = answerButtons.map(UiObject::getText) | |
if (answers.any(String::isBlank)) { | |
Log.w(LOG_TAG, "Warning: blank answer") | |
return | |
} | |
// Answer the question | |
val question = Question(questionText, answers) | |
val selectedAnswer = competitor[question] | |
if (selectedAnswer == null || selectedAnswer >= 0) { | |
val (x, y) = ANSWERS_POINTS[selectedAnswer ?: RANDOM_GUESS].first() | |
device.click(x, y) | |
} else return | |
sleep(TIMEOUT_WAIT_FOR_RESULT_FOR_ANSWERING) | |
// check what is the correct answer if possible | |
assert(device.takeScreenshot(BITMAP_FILE)) | |
val bitmap = BitmapFactory.decodeFile(BITMAP_FILE.absolutePath) | |
val correctAnswer = | |
ANSWERS_POINTS.indexOfFirst { it.any { (x, y) -> bitmap[x, y] == CORRECT_ANSWER_COLOR } } | |
.takeIf { it != -1 } | |
if (correctAnswer != null) { | |
competitor[question] = correctAnswer | |
} | |
// report to log | |
Log.i(LOG_TAG, "========= $question ===========") | |
Log.i(LOG_TAG, "${++answeredQuestions}) correct: $correctAnswer, answered: $selectedAnswer") | |
if (selectedAnswer != null && correctAnswer != null && selectedAnswer != correctAnswer) { | |
Log.w(LOG_TAG, "Mismatch: cached answer and correct answer are different") | |
} | |
if (correctAnswer == null) { | |
Log.w(LOG_TAG, "Fail: Could not capture correct answer") | |
} | |
if (question.question in questionsSeen) { | |
Log.w(LOG_TAG, "Repeat: question was seen already") | |
} | |
questionsSeen += question.question | |
Log.i(LOG_TAG, "====================================") | |
} | |
fun tryCloseAd() { | |
val xButton = device.findObject( | |
By.clazz(Button::class.java).clickable(true).hasChild( | |
By.clazz(Button::class.java) | |
) | |
) | |
if (xButton != null && xButton.childCount == 1 && xButton.parent.childCount == 2) { | |
xButton.children[0].click() | |
} | |
} | |
fun tryPlayAgain() { | |
device.findObject(By.text("שחק שוב"))?.let { | |
DATA_FILE.writeText(competitor.export()) | |
it.click() | |
} | |
} | |
companion object { | |
private const val TIMEOUT_WAIT_FOR_RESULT_FOR_ANSWERING = 100L | |
private const val LOG_TAG = "Aluf-bot" | |
private const val ANSWERS_COUNT = 4 | |
private const val RANDOM_GUESS = 2 | |
private const val LEFT_OF_ANSWER = 48 | |
private val ANSWERS_POINTS = listOf( | |
listOf(184 to 830), | |
listOf(184 to 1015), | |
listOf(184 to 1200), | |
listOf(184 to 1385) | |
) | |
private val CORRECT_ANSWER_COLOR = Color.parseColor("#2CB888") | |
private val BITMAP_FILE = File("/sdcard/bot_aluf_hamikraot.png") | |
private val DATA_FILE = File("/sdcard/bot_aluf_hamikraot.json") | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment